1 
2 //          Copyright Ferdinand Majerech 2011-2014.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 /// YAML scanner.
8 /// Code based on PyYAML: http://www.pyyaml.org
9 module dyaml.scanner;
10 
11 
12 import core.stdc..string;
13 
14 import std.algorithm;
15 import std.array;
16 import std.container;
17 import std.conv;
18 import std.ascii : isAlphaNum, isDigit, isHexDigit;
19 import std.exception;
20 import std..string;
21 import std.typecons;
22 import std.traits : Unqual;
23 
24 import dyaml.fastcharsearch;
25 import dyaml.escapes;
26 import dyaml.exception;
27 import dyaml.nogcutil;
28 import dyaml.queue;
29 import dyaml.reader;
30 import dyaml.style;
31 import dyaml.token;
32 
33 package:
34 /// Scanner produces tokens of the following types:
35 /// STREAM-START
36 /// STREAM-END
37 /// DIRECTIVE(name, value)
38 /// DOCUMENT-START
39 /// DOCUMENT-END
40 /// BLOCK-SEQUENCE-START
41 /// BLOCK-MAPPING-START
42 /// BLOCK-END
43 /// FLOW-SEQUENCE-START
44 /// FLOW-MAPPING-START
45 /// FLOW-SEQUENCE-END
46 /// FLOW-MAPPING-END
47 /// BLOCK-ENTRY
48 /// FLOW-ENTRY
49 /// KEY
50 /// VALUE
51 /// ALIAS(value)
52 /// ANCHOR(value)
53 /// TAG(value)
54 /// SCALAR(value, plain, style)
55 
56 
57 /// Marked exception thrown at scanner errors.
58 ///
59 /// See_Also: MarkedYAMLException
60 class ScannerException : MarkedYAMLException
61 {
62     mixin MarkedExceptionCtors;
63 }
64 
65 /// Generates tokens from data provided by a Reader.
66 final class Scanner
67 {
68     private:
69         /// A simple key is a key that is not denoted by the '?' indicator.
70         /// For example:
71         ///   ---
72         ///   block simple key: value
73         ///   ? not a simple key:
74         ///   : { flow simple key: value }
75         /// We emit the KEY token before all keys, so when we find a potential simple
76         /// key, we try to locate the corresponding ':' indicator. Simple keys should be
77         /// limited to a single line and 1024 characters.
78         ///
79         /// 16 bytes on 64-bit.
80         static struct SimpleKey
81         {
82             /// Character index in reader where the key starts.
83             uint charIndex = uint.max;
84             /// Index of the key token from start (first token scanned being 0).
85             uint tokenIndex;
86             /// Line the key starts at.
87             uint line;
88             /// Column the key starts at.
89             ushort column;
90             /// Is this required to be a simple key?
91             bool required;
92             /// Is this struct "null" (invalid)?.
93             bool isNull;
94         }
95 
96         /// Block chomping types.
97         enum Chomping
98         {
99             /// Strip all trailing line breaks. '-' indicator.
100             Strip,
101             /// Line break of the last line is preserved, others discarded. Default.
102             Clip,
103             /// All trailing line breaks are preserved. '+' indicator.
104             Keep
105         }
106 
107         /// Reader used to read from a file/stream.
108         Reader reader_;
109         /// Are we done scanning?
110         bool done_;
111 
112         /// Level of nesting in flow context. If 0, we're in block context.
113         uint flowLevel_;
114         /// Current indentation level.
115         int indent_ = -1;
116         /// Past indentation levels. Used as a stack.
117         Array!int indents_;
118 
119         /// Processed tokens not yet emitted. Used as a queue.
120         Queue!Token tokens_;
121 
122         /// Number of tokens emitted through the getToken method.
123         uint tokensTaken_;
124 
125         /// Can a simple key start at the current position? A simple key may start:
126         /// - at the beginning of the line, not counting indentation spaces
127         ///       (in block context),
128         /// - after '{', '[', ',' (in the flow context),
129         /// - after '?', ':', '-' (in the block context).
130         /// In the block context, this flag also signifies if a block collection
131         /// may start at the current position.
132         bool allowSimpleKey_ = true;
133 
134         /// Possible simple keys indexed by flow levels.
135         SimpleKey[] possibleSimpleKeys_;
136 
137 
138         /// Set on error by nothrow/@nogc inner functions along with errorData_.
139         ///
140         /// Non-nothrow/GC-using caller functions can then throw an exception using
141         /// data stored in errorData_.
142         bool error_;
143 
144         /// Data for the exception to throw if error_ is true.
145         MarkedYAMLExceptionData errorData_;
146 
147         /// Error messages can be built in this buffer without using the GC.
148         ///
149         /// ScannerException (MarkedYAMLException) copies string data passed to its
150         /// constructor so it's safe to use slices of this buffer as parameters for
151         /// exceptions that may outlive the Scanner. The GC allocation when creating the
152         /// error message is removed, but the allocation when creating an exception is
153         /// not.
154         char[256] msgBuffer_;
155 
156         /// Used to detect if a character is any whitespace plus '\0'
157         mixin FastCharSearch!" \t\0\n\r\u0085\u2028\u2029"d searchAllWhitespace;
158         /// Used to detect if a character is any line break plus '\0'
159         mixin FastCharSearch!"\0\n\r\u0085\u2028\u2029"d searchAllBreaks;
160 
161         /// Avoids compiler confusion of std.algorithm.canFind with FastCharSearch.
162         alias canFind = std.algorithm.canFind;
163 
164     public:
165         /// Construct a Scanner using specified Reader.
166         this(Reader reader) @safe nothrow
167         {
168             // Return the next token, but do not delete it from the queue
169             reader_   = reader;
170             fetchStreamStart();
171         }
172 
173         /// Destroy the scanner.
174         @trusted ~this()
175         {
176             tokens_.destroy();
177             indents_.destroy();
178             possibleSimpleKeys_.destroy();
179             possibleSimpleKeys_ = null;
180             reader_ = null;
181         }
182 
183         /// Check if the next token is one of specified types.
184         ///
185         /// If no types are specified, checks if any tokens are left.
186         ///
187         /// Params:  ids = Token IDs to check for.
188         ///
189         /// Returns: true if the next token is one of specified types, or if there are
190         ///          any tokens left if no types specified, false otherwise.
191         bool checkToken(const TokenID[] ids ...) @safe
192         {
193             // Check if the next token is one of specified types.
194             while(needMoreTokens()) { fetchToken(); }
195             if(!tokens_.empty)
196             {
197                 if(ids.length == 0) { return true; }
198                 else
199                 {
200                     const nextId = tokens_.peek().id;
201                     foreach(id; ids)
202                     {
203                         if(nextId == id) { return true; }
204                     }
205                 }
206             }
207             return false;
208         }
209 
210         /// Return the next token, but keep it in the queue.
211         ///
212         /// Must not be called if there are no tokens left.
213         ref const(Token) peekToken() @safe
214         {
215             while(needMoreTokens) { fetchToken(); }
216             if(!tokens_.empty)    { return tokens_.peek(); }
217             assert(false, "No token left to peek");
218         }
219 
220         /// Return the next token, removing it from the queue.
221         ///
222         /// Must not be called if there are no tokens left.
223         Token getToken() @safe
224         {
225             while(needMoreTokens){fetchToken();}
226             if(!tokens_.empty)
227             {
228                 ++tokensTaken_;
229                 return tokens_.pop();
230             }
231             assert(false, "No token left to get");
232         }
233 
234     private:
235         /// Build an error message in msgBuffer_ and return it as a string.
236         string buildMsg(S ...)(S args) @trusted pure nothrow @nogc
237         {
238             return cast(string)msgBuffer_.printNoGC(args);
239         }
240 
241         /// Most scanning error messages have the same format; so build them with this
242         /// function.
243         string expected(T)(string expected, T found) @safe pure nothrow @nogc
244         {
245             return buildMsg("expected ", expected, ", but found ", found);
246         }
247 
248         /// If error_ is true, throws a ScannerException constructed from errorData_ and
249         /// sets error_ to false.
250         void throwIfError() @safe pure
251         {
252             if(!error_) { return; }
253             error_ = false;
254             throw new ScannerException(errorData_);
255         }
256 
257         /// Called by internal nothrow/@nogc methods to set an error to be thrown by
258         /// their callers.
259         ///
260         /// See_Also: dyaml.exception.MarkedYamlException
261         void error(string context, const Mark contextMark, string problem,
262                    const Mark problemMark) @safe pure nothrow @nogc
263         {
264             assert(error_ == false,
265                    "Setting an error when there already is a not yet thrown error");
266             error_     = true;
267             errorData_ = MarkedYAMLExceptionData(context, contextMark, problem, problemMark);
268         }
269 
270         /// Determine whether or not we need to fetch more tokens before peeking/getting a token.
271         bool needMoreTokens() @safe pure
272         {
273             if(done_)         { return false; }
274             if(tokens_.empty) { return true; }
275 
276             /// The current token may be a potential simple key, so we need to look further.
277             stalePossibleSimpleKeys();
278             return nextPossibleSimpleKey() == tokensTaken_;
279         }
280 
281         /// Fetch at token, adding it to tokens_.
282         void fetchToken() @safe
283         {
284             // Eat whitespaces and comments until we reach the next token.
285             scanToNextToken();
286 
287             // Remove obsolete possible simple keys.
288             stalePossibleSimpleKeys();
289 
290             // Compare current indentation and column. It may add some tokens
291             // and decrease the current indentation level.
292             unwindIndent(reader_.column);
293 
294             // Get the next character.
295             const dchar c = reader_.peekByte();
296 
297             // Fetch the token.
298             if(c == '\0')            { return fetchStreamEnd();     }
299             if(checkDirective())     { return fetchDirective();     }
300             if(checkDocumentStart()) { return fetchDocumentStart(); }
301             if(checkDocumentEnd())   { return fetchDocumentEnd();   }
302             // Order of the following checks is NOT significant.
303             switch(c)
304             {
305                 case '[':  return fetchFlowSequenceStart();
306                 case '{':  return fetchFlowMappingStart();
307                 case ']':  return fetchFlowSequenceEnd();
308                 case '}':  return fetchFlowMappingEnd();
309                 case ',':  return fetchFlowEntry();
310                 case '!':  return fetchTag();
311                 case '\'': return fetchSingle();
312                 case '\"': return fetchDouble();
313                 case '*':  return fetchAlias();
314                 case '&':  return fetchAnchor();
315                 case '?':  if(checkKey())        { return fetchKey();        } goto default;
316                 case ':':  if(checkValue())      { return fetchValue();      } goto default;
317                 case '-':  if(checkBlockEntry()) { return fetchBlockEntry(); } goto default;
318                 case '|':  if(flowLevel_ == 0)   { return fetchLiteral();    } break;
319                 case '>':  if(flowLevel_ == 0)   { return fetchFolded();     } break;
320                 default:   if(checkPlain())      { return fetchPlain();      }
321             }
322 
323             throw new ScannerException("While scanning for the next token, found character "
324                                        "\'%s\', index %s that cannot start any token"
325                                        .format(c, to!int(c)), reader_.mark);
326         }
327 
328 
329         /// Return the token number of the nearest possible simple key.
330         uint nextPossibleSimpleKey() @safe pure nothrow @nogc
331         {
332             uint minTokenNumber = uint.max;
333             foreach(k, ref simpleKey; possibleSimpleKeys_)
334             {
335                 if(simpleKey.isNull) { continue; }
336                 minTokenNumber = min(minTokenNumber, simpleKey.tokenIndex);
337             }
338             return minTokenNumber;
339         }
340 
341         /// Remove entries that are no longer possible simple keys.
342         ///
343         /// According to the YAML specification, simple keys
344         /// - should be limited to a single line,
345         /// - should be no longer than 1024 characters.
346         /// Disabling this will allow simple keys of any length and
347         /// height (may cause problems if indentation is broken though).
348         void stalePossibleSimpleKeys() @safe pure
349         {
350             foreach(level, ref key; possibleSimpleKeys_)
351             {
352                 if(key.isNull) { continue; }
353                 if(key.line != reader_.line || reader_.charIndex - key.charIndex > 1024)
354                 {
355                     enforce(!key.required,
356                             new ScannerException("While scanning a simple key",
357                                                  Mark(key.line, key.column),
358                                                  "could not find expected ':'", reader_.mark));
359                     key.isNull = true;
360                 }
361             }
362         }
363 
364         /// Check if the next token starts a possible simple key and if so, save its position.
365         ///
366         /// This function is called for ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'.
367         void savePossibleSimpleKey() @safe pure
368         {
369             // Check if a simple key is required at the current position.
370             const required = (flowLevel_ == 0 && indent_ == reader_.column);
371             assert(allowSimpleKey_ || !required, "A simple key is required only if it is "
372                    "the first token in the current line. Therefore it is always allowed.");
373 
374             if(!allowSimpleKey_) { return; }
375 
376             // The next token might be a simple key, so save its number and position.
377             removePossibleSimpleKey();
378             const tokenCount = tokensTaken_ + cast(uint)tokens_.length;
379 
380             const line   = reader_.line;
381             const column = reader_.column;
382             const key    = SimpleKey(cast(uint)reader_.charIndex, tokenCount, line,
383                                      cast(ushort)min(column, ushort.max), required);
384 
385             if(possibleSimpleKeys_.length <= flowLevel_)
386             {
387                 const oldLength = possibleSimpleKeys_.length;
388                 possibleSimpleKeys_.length = flowLevel_ + 1;
389                 //No need to initialize the last element, it's already done in the next line.
390                 possibleSimpleKeys_[oldLength .. flowLevel_] = SimpleKey.init;
391             }
392             possibleSimpleKeys_[flowLevel_] = key;
393         }
394 
395         /// Remove the saved possible key position at the current flow level.
396         void removePossibleSimpleKey() @safe pure
397         {
398             if(possibleSimpleKeys_.length <= flowLevel_) { return; }
399 
400             if(!possibleSimpleKeys_[flowLevel_].isNull)
401             {
402                 const key = possibleSimpleKeys_[flowLevel_];
403                 enforce(!key.required,
404                         new ScannerException("While scanning a simple key",
405                                              Mark(key.line, key.column),
406                                              "could not find expected ':'", reader_.mark));
407                 possibleSimpleKeys_[flowLevel_].isNull = true;
408             }
409         }
410 
411         /// Decrease indentation, removing entries in indents_.
412         ///
413         /// Params:  column = Current column in the file/stream.
414         void unwindIndent(const int column) @trusted
415         {
416             if(flowLevel_ > 0)
417             {
418                 // In flow context, tokens should respect indentation.
419                 // The condition should be `indent >= column` according to the spec.
420                 // But this condition will prohibit intuitively correct
421                 // constructions such as
422                 // key : {
423                 // }
424 
425                 // In the flow context, indentation is ignored. We make the scanner less
426                 // restrictive than what the specification requires.
427                 // if(pedantic_ && flowLevel_ > 0 && indent_ > column)
428                 // {
429                 //     throw new ScannerException("Invalid intendation or unclosed '[' or '{'",
430                 //                                reader_.mark)
431                 // }
432                 return;
433             }
434 
435             // In block context, we may need to issue the BLOCK-END tokens.
436             while(indent_ > column)
437             {
438                 indent_ = indents_.back;
439                 indents_.length = indents_.length - 1;
440                 tokens_.push(blockEndToken(reader_.mark, reader_.mark));
441             }
442         }
443 
444         /// Increase indentation if needed.
445         ///
446         /// Params:  column = Current column in the file/stream.
447         ///
448         /// Returns: true if the indentation was increased, false otherwise.
449         bool addIndent(int column) @trusted
450         {
451             if(indent_ >= column){return false;}
452             indents_ ~= indent_;
453             indent_ = column;
454             return true;
455         }
456 
457 
458         /// Add STREAM-START token.
459         void fetchStreamStart() @safe nothrow
460         {
461             tokens_.push(streamStartToken(reader_.mark, reader_.mark, reader_.encoding));
462         }
463 
464         ///Add STREAM-END token.
465         void fetchStreamEnd() @safe
466         {
467             //Set intendation to -1 .
468             unwindIndent(-1);
469             removePossibleSimpleKey();
470             allowSimpleKey_ = false;
471             possibleSimpleKeys_.destroy;
472 
473             tokens_.push(streamEndToken(reader_.mark, reader_.mark));
474             done_ = true;
475         }
476 
477         /// Add DIRECTIVE token.
478         void fetchDirective() @safe
479         {
480             // Set intendation to -1 .
481             unwindIndent(-1);
482             // Reset simple keys.
483             removePossibleSimpleKey();
484             allowSimpleKey_ = false;
485 
486             auto directive = scanDirective();
487             throwIfError();
488             tokens_.push(directive);
489         }
490 
491         /// Add DOCUMENT-START or DOCUMENT-END token.
492         void fetchDocumentIndicator(TokenID id)() @safe
493             if(id == TokenID.DocumentStart || id == TokenID.DocumentEnd)
494         {
495             // Set indentation to -1 .
496             unwindIndent(-1);
497             // Reset simple keys. Note that there can't be a block collection after '---'.
498             removePossibleSimpleKey();
499             allowSimpleKey_ = false;
500 
501             Mark startMark = reader_.mark;
502             reader_.forward(3);
503             tokens_.push(simpleToken!id(startMark, reader_.mark));
504         }
505 
506         /// Aliases to add DOCUMENT-START or DOCUMENT-END token.
507         alias fetchDocumentIndicator!(TokenID.DocumentStart) fetchDocumentStart;
508         alias fetchDocumentIndicator!(TokenID.DocumentEnd) fetchDocumentEnd;
509 
510         /// Add FLOW-SEQUENCE-START or FLOW-MAPPING-START token.
511         void fetchFlowCollectionStart(TokenID id)() @trusted
512         {
513             // '[' and '{' may start a simple key.
514             savePossibleSimpleKey();
515             // Simple keys are allowed after '[' and '{'.
516             allowSimpleKey_ = true;
517             ++flowLevel_;
518 
519             Mark startMark = reader_.mark;
520             reader_.forward();
521             tokens_.push(simpleToken!id(startMark, reader_.mark));
522         }
523 
524         /// Aliases to add FLOW-SEQUENCE-START or FLOW-MAPPING-START token.
525         alias fetchFlowCollectionStart!(TokenID.FlowSequenceStart) fetchFlowSequenceStart;
526         alias fetchFlowCollectionStart!(TokenID.FlowMappingStart) fetchFlowMappingStart;
527 
528         /// Add FLOW-SEQUENCE-START or FLOW-MAPPING-START token.
529         void fetchFlowCollectionEnd(TokenID id)() @safe
530         {
531             // Reset possible simple key on the current level.
532             removePossibleSimpleKey();
533             // No simple keys after ']' and '}'.
534             allowSimpleKey_ = false;
535             --flowLevel_;
536 
537             Mark startMark = reader_.mark;
538             reader_.forward();
539             tokens_.push(simpleToken!id(startMark, reader_.mark));
540         }
541 
542         /// Aliases to add FLOW-SEQUENCE-START or FLOW-MAPPING-START token/
543         alias fetchFlowCollectionEnd!(TokenID.FlowSequenceEnd) fetchFlowSequenceEnd;
544         alias fetchFlowCollectionEnd!(TokenID.FlowMappingEnd) fetchFlowMappingEnd;
545 
546         /// Add FLOW-ENTRY token;
547         void fetchFlowEntry() @safe
548         {
549             // Reset possible simple key on the current level.
550             removePossibleSimpleKey();
551             // Simple keys are allowed after ','.
552             allowSimpleKey_ = true;
553 
554             Mark startMark = reader_.mark;
555             reader_.forward();
556             tokens_.push(flowEntryToken(startMark, reader_.mark));
557         }
558 
559         /// Additional checks used in block context in fetchBlockEntry and fetchKey.
560         ///
561         /// Params:  type = String representing the token type we might need to add.
562         ///          id   = Token type we might need to add.
563         void blockChecks(string type, TokenID id)() @safe
564         {
565             enum context = type ~ " keys are not allowed here";
566             // Are we allowed to start a key (not neccesarily a simple one)?
567             enforce(allowSimpleKey_, new ScannerException(context, reader_.mark));
568 
569             if(addIndent(reader_.column))
570             {
571                 tokens_.push(simpleToken!id(reader_.mark, reader_.mark));
572             }
573         }
574 
575         /// Add BLOCK-ENTRY token. Might add BLOCK-SEQUENCE-START in the process.
576         void fetchBlockEntry() @safe
577         {
578             if(flowLevel_ == 0) { blockChecks!("Sequence", TokenID.BlockSequenceStart)(); }
579 
580             // It's an error for the block entry to occur in the flow context,
581             // but we let the parser detect this.
582 
583             // Reset possible simple key on the current level.
584             removePossibleSimpleKey();
585             // Simple keys are allowed after '-'.
586             allowSimpleKey_ = true;
587 
588             Mark startMark = reader_.mark;
589             reader_.forward();
590             tokens_.push(blockEntryToken(startMark, reader_.mark));
591         }
592 
593         /// Add KEY token. Might add BLOCK-MAPPING-START in the process.
594         void fetchKey() @safe
595         {
596             if(flowLevel_ == 0) { blockChecks!("Mapping", TokenID.BlockMappingStart)(); }
597 
598             // Reset possible simple key on the current level.
599             removePossibleSimpleKey();
600             // Simple keys are allowed after '?' in the block context.
601             allowSimpleKey_ = (flowLevel_ == 0);
602 
603             Mark startMark = reader_.mark;
604             reader_.forward();
605             tokens_.push(keyToken(startMark, reader_.mark));
606         }
607 
608         /// Add VALUE token. Might add KEY and/or BLOCK-MAPPING-START in the process.
609         void fetchValue() @safe
610         {
611             //Do we determine a simple key?
612             if(possibleSimpleKeys_.length > flowLevel_ &&
613                !possibleSimpleKeys_[flowLevel_].isNull)
614             {
615                 const key = possibleSimpleKeys_[flowLevel_];
616                 possibleSimpleKeys_[flowLevel_].isNull = true;
617                 Mark keyMark = Mark(key.line, key.column);
618                 const idx = key.tokenIndex - tokensTaken_;
619 
620                 assert(idx >= 0);
621 
622                 // Add KEY.
623                 // Manually inserting since tokens are immutable (need linked list).
624                 tokens_.insert(keyToken(keyMark, keyMark), idx);
625 
626                 // If this key starts a new block mapping, we need to add BLOCK-MAPPING-START.
627                 if(flowLevel_ == 0 && addIndent(key.column))
628                 {
629                     tokens_.insert(blockMappingStartToken(keyMark, keyMark), idx);
630                 }
631 
632                 // There cannot be two simple keys in a row.
633                 allowSimpleKey_ = false;
634             }
635             // Part of a complex key
636             else
637             {
638                 // We can start a complex value if and only if we can start a simple key.
639                 enforce(flowLevel_ > 0 || allowSimpleKey_,
640                         new ScannerException("Mapping values are not allowed here", reader_.mark));
641 
642                 // If this value starts a new block mapping, we need to add
643                 // BLOCK-MAPPING-START. It'll be detected as an error later by the parser.
644                 if(flowLevel_ == 0 && addIndent(reader_.column))
645                 {
646                     tokens_.push(blockMappingStartToken(reader_.mark, reader_.mark));
647                 }
648 
649                 // Reset possible simple key on the current level.
650                 removePossibleSimpleKey();
651                 // Simple keys are allowed after ':' in the block context.
652                 allowSimpleKey_ = (flowLevel_ == 0);
653             }
654 
655             // Add VALUE.
656             Mark startMark = reader_.mark;
657             reader_.forward();
658             tokens_.push(valueToken(startMark, reader_.mark));
659         }
660 
661         /// Add ALIAS or ANCHOR token.
662         void fetchAnchor_(TokenID id)() @trusted
663             if(id == TokenID.Alias || id == TokenID.Anchor)
664         {
665             // ALIAS/ANCHOR could be a simple key.
666             savePossibleSimpleKey();
667             // No simple keys after ALIAS/ANCHOR.
668             allowSimpleKey_ = false;
669 
670             auto anchor = scanAnchor(id);
671             throwIfError();
672             tokens_.push(anchor);
673         }
674 
675         /// Aliases to add ALIAS or ANCHOR token.
676         alias fetchAnchor_!(TokenID.Alias) fetchAlias;
677         alias fetchAnchor_!(TokenID.Anchor) fetchAnchor;
678 
679         /// Add TAG token.
680         void fetchTag() @trusted
681         {
682             //TAG could start a simple key.
683             savePossibleSimpleKey();
684             //No simple keys after TAG.
685             allowSimpleKey_ = false;
686 
687             tokens_.push(scanTag());
688             throwIfError();
689         }
690 
691         /// Add block SCALAR token.
692         void fetchBlockScalar(ScalarStyle style)() @trusted
693             if(style == ScalarStyle.Literal || style == ScalarStyle.Folded)
694         {
695             // Reset possible simple key on the current level.
696             removePossibleSimpleKey();
697             // A simple key may follow a block scalar.
698             allowSimpleKey_ = true;
699 
700             auto blockScalar = scanBlockScalar(style);
701             throwIfError();
702             tokens_.push(blockScalar);
703         }
704 
705         /// Aliases to add literal or folded block scalar.
706         alias fetchBlockScalar!(ScalarStyle.Literal) fetchLiteral;
707         alias fetchBlockScalar!(ScalarStyle.Folded) fetchFolded;
708 
709         /// Add quoted flow SCALAR token.
710         void fetchFlowScalar(ScalarStyle quotes)() @safe
711         {
712             // A flow scalar could be a simple key.
713             savePossibleSimpleKey();
714             // No simple keys after flow scalars.
715             allowSimpleKey_ = false;
716 
717             // Scan and add SCALAR.
718             auto scalar = scanFlowScalar(quotes);
719             throwIfError();
720             tokens_.push(scalar);
721         }
722 
723         /// Aliases to add single or double quoted block scalar.
724         alias fetchFlowScalar!(ScalarStyle.SingleQuoted) fetchSingle;
725         alias fetchFlowScalar!(ScalarStyle.DoubleQuoted) fetchDouble;
726 
727         /// Add plain SCALAR token.
728         void fetchPlain() @safe
729         {
730             // A plain scalar could be a simple key
731             savePossibleSimpleKey();
732             // No simple keys after plain scalars. But note that scanPlain() will
733             // change this flag if the scan is finished at the beginning of the line.
734             allowSimpleKey_ = false;
735             auto plain = scanPlain();
736             throwIfError();
737 
738             // Scan and add SCALAR. May change allowSimpleKey_
739             tokens_.push(plain);
740         }
741 
742     pure nothrow @nogc:
743 
744         ///Check if the next token is DIRECTIVE:        ^ '%' ...
745         bool checkDirective() @safe
746         {
747             return reader_.peekByte() == '%' && reader_.column == 0;
748         }
749 
750         /// Check if the next token is DOCUMENT-START:   ^ '---' (' '|'\n')
751         bool checkDocumentStart() @safe
752         {
753             // Check one char first, then all 3, to prevent reading outside the buffer.
754             return reader_.column     == 0     &&
755                    reader_.peekByte() == '-'   &&
756                    reader_.prefix(3)  == "---" &&
757                    searchAllWhitespace.canFind(reader_.peek(3));
758         }
759 
760         /// Check if the next token is DOCUMENT-END:     ^ '...' (' '|'\n')
761         bool checkDocumentEnd() @safe
762         {
763             // Check one char first, then all 3, to prevent reading outside the buffer.
764             return reader_.column     == 0     &&
765                    reader_.peekByte() == '.'   &&
766                    reader_.prefix(3)  == "..." &&
767                    searchAllWhitespace.canFind(reader_.peek(3));
768         }
769 
770         /// Check if the next token is BLOCK-ENTRY:      '-' (' '|'\n')
771         bool checkBlockEntry() @safe
772         {
773             return searchAllWhitespace.canFind(reader_.peek(1));
774         }
775 
776         /// Check if the next token is KEY(flow context):    '?'
777         ///
778         /// or KEY(block context):   '?' (' '|'\n')
779         bool checkKey() @safe
780         {
781             return (flowLevel_ > 0 || searchAllWhitespace.canFind(reader_.peek(1)));
782         }
783 
784         /// Check if the next token is VALUE(flow context):  ':'
785         ///
786         /// or VALUE(block context): ':' (' '|'\n')
787         bool checkValue() @safe
788         {
789             return flowLevel_ > 0 || searchAllWhitespace.canFind(reader_.peek(1));
790         }
791 
792         /// Check if the next token is a plain scalar.
793         ///
794         /// A plain scalar may start with any non-space character except:
795         ///   '-', '?', ':', ',', '[', ']', '{', '}',
796         ///   '#', '&', '*', '!', '|', '>', '\'', '\"',
797         ///   '%', '@', '`'.
798         ///
799         /// It may also start with
800         ///   '-', '?', ':'
801         /// if it is followed by a non-space character.
802         ///
803         /// Note that we limit the last rule to the block context (except the
804         /// '-' character) because we want the flow context to be space
805         /// independent.
806         bool checkPlain() @safe
807         {
808             const c = reader_.peek();
809             mixin FastCharSearch!"-?:,[]{}#&*!|>\'\"%@` \t\0\n\r\u0085\u2028\u2029"d
810                 searchPlainNotFirstChar;
811             if(!searchPlainNotFirstChar.canFind(c))
812             {
813                 return true;
814             }
815             return !searchAllWhitespace.canFind(reader_.peek(1)) &&
816                    (c == '-' || (flowLevel_ == 0 && (c == '?' || c == ':')));
817         }
818 
819         /// Move to the next non-space character.
820         void findNextNonSpace() @safe
821         {
822             while(reader_.peekByte() == ' ') { reader_.forward(); }
823         }
824 
825         /// Scan a string of alphanumeric or "-_" characters.
826         ///
827         /// Assumes that the caller is building a slice in Reader, and puts the scanned
828         /// characters into that slice.
829         ///
830         /// In case of an error, error_ is set. Use throwIfError() to handle this.
831         void scanAlphaNumericToSlice(string name)(const Mark startMark) @system
832         {
833             size_t length = 0;
834             dchar c = reader_.peek();
835             while(c.isAlphaNum || "-_"d.canFind(c)) { c = reader_.peek(++length); }
836 
837             if(length == 0)
838             {
839                 enum contextMsg = "While scanning " ~ name;
840                 error(contextMsg, startMark, expected("alphanumeric, '-' or '_'", c),
841                       reader_.mark);
842                 return;
843             }
844 
845             reader_.sliceBuilder.write(reader_.get(length));
846         }
847 
848         /// Scan and throw away all characters until next line break.
849         void scanToNextBreak() @safe
850         {
851             while(!searchAllBreaks.canFind(reader_.peek())) { reader_.forward(); }
852         }
853 
854         /// Scan all characters until next line break.
855         ///
856         /// Assumes that the caller is building a slice in Reader, and puts the scanned
857         /// characters into that slice.
858         void scanToNextBreakToSlice() @system
859         {
860             uint length = 0;
861             while(!searchAllBreaks.canFind(reader_.peek(length)))
862             {
863                 ++length;
864             }
865             reader_.sliceBuilder.write(reader_.get(length));
866         }
867 
868 
869         /// Move to next token in the file/stream.
870         ///
871         /// We ignore spaces, line breaks and comments.
872         /// If we find a line break in the block context, we set
873         /// allowSimpleKey` on.
874         ///
875         /// We do not yet support BOM inside the stream as the
876         /// specification requires. Any such mark will be considered as a part
877         /// of the document.
878         void scanToNextToken() @safe
879         {
880             // TODO(PyYAML): We need to make tab handling rules more sane. A good rule is:
881             //   Tabs cannot precede tokens
882             //   BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END,
883             //   KEY(block), VALUE(block), BLOCK-ENTRY
884             // So the checking code is
885             //   if <TAB>:
886             //       allowSimpleKey_ = false
887             // We also need to add the check for `allowSimpleKey_ == true` to
888             // `unwindIndent` before issuing BLOCK-END.
889             // Scanners for block, flow, and plain scalars need to be modified.
890 
891             for(;;)
892             {
893                 findNextNonSpace();
894 
895                 if(reader_.peekByte() == '#') { scanToNextBreak(); }
896                 if(scanLineBreak() != '\0')
897                 {
898                     if(flowLevel_ == 0) { allowSimpleKey_ = true; }
899                 }
900                 else
901                 {
902                     break;
903                 }
904             }
905         }
906 
907         /// Scan directive token.
908         Token scanDirective() @trusted
909         {
910             Mark startMark = reader_.mark;
911             // Skip the '%'.
912             reader_.forward();
913 
914             // Scan directive name
915             reader_.sliceBuilder.begin();
916             scanDirectiveNameToSlice(startMark);
917             if(error_) { return Token.init; }
918             const name = reader_.sliceBuilder.finish();
919 
920             reader_.sliceBuilder.begin();
921 
922             // Index where tag handle ends and suffix starts in a tag directive value.
923             uint tagHandleEnd = uint.max;
924             if(name == "YAML")     { scanYAMLDirectiveValueToSlice(startMark); }
925             else if(name == "TAG") { tagHandleEnd = scanTagDirectiveValueToSlice(startMark); }
926             if(error_) { return Token.init; }
927             char[] value = reader_.sliceBuilder.finish();
928 
929             Mark endMark = reader_.mark;
930 
931             DirectiveType directive;
932             if(name == "YAML")     { directive = DirectiveType.YAML; }
933             else if(name == "TAG") { directive = DirectiveType.TAG; }
934             else
935             {
936                 directive = DirectiveType.Reserved;
937                 scanToNextBreak();
938             }
939 
940             scanDirectiveIgnoredLine(startMark);
941             if(error_) { return Token.init; }
942 
943             return directiveToken(startMark, endMark, value, directive, tagHandleEnd);
944         }
945 
946         /// Scan name of a directive token.
947         ///
948         /// Assumes that the caller is building a slice in Reader, and puts the scanned
949         /// characters into that slice.
950         ///
951         /// In case of an error, error_ is set. Use throwIfError() to handle this.
952         void scanDirectiveNameToSlice(const Mark startMark) @system
953         {
954             // Scan directive name.
955             scanAlphaNumericToSlice!"a directive"(startMark);
956             if(error_) { return; }
957 
958             if(" \0\n\r\u0085\u2028\u2029"d.canFind(reader_.peek())) { return; }
959             error("While scanning a directive", startMark,
960                   expected("alphanumeric, '-' or '_'", reader_.peek()), reader_.mark);
961         }
962 
963         /// Scan value of a YAML directive token. Returns major, minor version separated by '.'.
964         ///
965         /// Assumes that the caller is building a slice in Reader, and puts the scanned
966         /// characters into that slice.
967         ///
968         /// In case of an error, error_ is set. Use throwIfError() to handle this.
969         void scanYAMLDirectiveValueToSlice(const Mark startMark) @system
970         {
971             findNextNonSpace();
972 
973             scanYAMLDirectiveNumberToSlice(startMark);
974             if(error_) { return; }
975 
976             if(reader_.peekByte() != '.')
977             {
978                 error("While scanning a directive", startMark,
979                       expected("digit or '.'", reader_.peek()), reader_.mark);
980                 return;
981             }
982             // Skip the '.'.
983             reader_.forward();
984 
985             reader_.sliceBuilder.write('.');
986             scanYAMLDirectiveNumberToSlice(startMark);
987             if(error_) { return; }
988 
989             if(!" \0\n\r\u0085\u2028\u2029"d.canFind(reader_.peek()))
990             {
991                 error("While scanning a directive", startMark,
992                       expected("digit or '.'", reader_.peek()), reader_.mark);
993             }
994         }
995 
996         /// Scan a number from a YAML directive.
997         ///
998         /// Assumes that the caller is building a slice in Reader, and puts the scanned
999         /// characters into that slice.
1000         ///
1001         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1002         void scanYAMLDirectiveNumberToSlice(const Mark startMark) @system
1003         {
1004             if(!isDigit(reader_.peek()))
1005             {
1006                 error("While scanning a directive", startMark,
1007                       expected("digit", reader_.peek()), reader_.mark);
1008                 return;
1009             }
1010 
1011             // Already found the first digit in the enforce(), so set length to 1.
1012             uint length = 1;
1013             while(reader_.peek(length).isDigit) { ++length; }
1014 
1015             reader_.sliceBuilder.write(reader_.get(length));
1016         }
1017 
1018         /// Scan value of a tag directive.
1019         ///
1020         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1021         /// characters into that slice.
1022         ///
1023         /// Returns: Length of tag handle (which is before tag prefix) in scanned data
1024         ///
1025         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1026         uint scanTagDirectiveValueToSlice(const Mark startMark) @system
1027         {
1028             findNextNonSpace();
1029             const startLength = reader_.sliceBuilder.length;
1030             scanTagDirectiveHandleToSlice(startMark);
1031             if(error_) { return uint.max; }
1032             const handleLength = cast(uint)(reader_.sliceBuilder.length  - startLength);
1033             findNextNonSpace();
1034             scanTagDirectivePrefixToSlice(startMark);
1035 
1036             return handleLength;
1037         }
1038 
1039         /// Scan handle of a tag directive.
1040         ///
1041         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1042         /// characters into that slice.
1043         ///
1044         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1045         void scanTagDirectiveHandleToSlice(const Mark startMark) @system
1046         {
1047             scanTagHandleToSlice!"directive"(startMark);
1048             if(error_) { return; }
1049             if(reader_.peekByte() == ' ') { return; }
1050             error("While scanning a directive handle", startMark,
1051                   expected("' '", reader_.peek()), reader_.mark);
1052         }
1053 
1054         /// Scan prefix of a tag directive.
1055         ///
1056         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1057         /// characters into that slice.
1058         ///
1059         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1060         void scanTagDirectivePrefixToSlice(const Mark startMark) @system
1061         {
1062             scanTagURIToSlice!"directive"(startMark);
1063             if(" \0\n\r\u0085\u2028\u2029"d.canFind(reader_.peek())) { return; }
1064             error("While scanning a directive prefix", startMark,
1065                   expected("' '", reader_.peek()), reader_.mark);
1066         }
1067 
1068         /// Scan (and ignore) ignored line after a directive.
1069         ///
1070         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1071         void scanDirectiveIgnoredLine(const Mark startMark) @safe
1072         {
1073             findNextNonSpace();
1074             if(reader_.peekByte() == '#') { scanToNextBreak(); }
1075             if(searchAllBreaks.canFind(reader_.peek()))
1076             {
1077                 scanLineBreak();
1078                 return;
1079             }
1080             error("While scanning a directive", startMark,
1081                   expected("comment or a line break", reader_.peek()), reader_.mark);
1082         }
1083 
1084 
1085         /// Scan an alias or an anchor.
1086         ///
1087         /// The specification does not restrict characters for anchors and
1088         /// aliases. This may lead to problems, for instance, the document:
1089         ///   [ *alias, value ]
1090         /// can be interpteted in two ways, as
1091         ///   [ "value" ]
1092         /// and
1093         ///   [ *alias , "value" ]
1094         /// Therefore we restrict aliases to ASCII alphanumeric characters.
1095         ///
1096         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1097         Token scanAnchor(const TokenID id) @trusted
1098         {
1099             const startMark = reader_.mark;
1100             const dchar i = reader_.get();
1101 
1102             reader_.sliceBuilder.begin();
1103             if(i == '*') { scanAlphaNumericToSlice!"an alias"(startMark); }
1104             else         { scanAlphaNumericToSlice!"an anchor"(startMark); }
1105             // On error, value is discarded as we return immediately
1106             char[] value = reader_.sliceBuilder.finish();
1107             if(error_)   { return Token.init; }
1108 
1109             if(!searchAllWhitespace.canFind(reader_.peek()) &&
1110                !"?:,]}%@"d.canFind(reader_.peekByte()))
1111             {
1112                 enum anchorCtx = "While scanning an anchor";
1113                 enum aliasCtx  = "While scanning an alias";
1114                 error(i == '*' ? aliasCtx : anchorCtx, startMark,
1115                       expected("alphanumeric, '-' or '_'", reader_.peek()), reader_.mark);
1116                 return Token.init;
1117             }
1118 
1119             if(id == TokenID.Alias)
1120             {
1121                 return aliasToken(startMark, reader_.mark, value);
1122             }
1123             if(id == TokenID.Anchor)
1124             {
1125                 return anchorToken(startMark, reader_.mark, value);
1126             }
1127             assert(false, "This code should never be reached");
1128         }
1129 
1130         /// Scan a tag token.
1131         ///
1132         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1133         Token scanTag() @trusted
1134         {
1135             const startMark = reader_.mark;
1136             dchar c = reader_.peek(1);
1137 
1138             reader_.sliceBuilder.begin();
1139             scope(failure) { reader_.sliceBuilder.finish(); }
1140             // Index where tag handle ends and tag suffix starts in the tag value
1141             // (slice) we will produce.
1142             uint handleEnd;
1143 
1144             mixin FastCharSearch!" \0\n\r\u0085\u2028\u2029"d search;
1145             if(c == '<')
1146             {
1147                 reader_.forward(2);
1148 
1149                 handleEnd = 0;
1150                 scanTagURIToSlice!"tag"(startMark);
1151                 if(error_) { return Token.init; }
1152                 if(reader_.peekByte() != '>')
1153                 {
1154                     error("While scanning a tag", startMark,
1155                           expected("'>'", reader_.peek()), reader_.mark);
1156                     return Token.init;
1157                 }
1158                 reader_.forward();
1159             }
1160             else if(searchAllWhitespace.canFind(c))
1161             {
1162                 reader_.forward();
1163                 handleEnd = 0;
1164                 reader_.sliceBuilder.write('!');
1165             }
1166             else
1167             {
1168                 uint length = 1;
1169                 bool useHandle = false;
1170 
1171                 while(!search.canFind(c))
1172                 {
1173                     if(c == '!')
1174                     {
1175                         useHandle = true;
1176                         break;
1177                     }
1178                     ++length;
1179                     c = reader_.peek(length);
1180                 }
1181 
1182                 if(useHandle)
1183                 {
1184                     scanTagHandleToSlice!"tag"(startMark);
1185                     handleEnd = cast(uint)reader_.sliceBuilder.length;
1186                     if(error_) { return Token.init; }
1187                 }
1188                 else
1189                 {
1190                     reader_.forward();
1191                     reader_.sliceBuilder.write('!');
1192                     handleEnd = cast(uint)reader_.sliceBuilder.length;
1193                 }
1194 
1195                 scanTagURIToSlice!"tag"(startMark);
1196                 if(error_) { return Token.init; }
1197             }
1198 
1199             if(search.canFind(reader_.peek()))
1200             {
1201                 char[] slice = reader_.sliceBuilder.finish();
1202                 return tagToken(startMark, reader_.mark, slice, handleEnd);
1203             }
1204 
1205             error("While scanning a tag", startMark, expected("' '", reader_.peek()),
1206                   reader_.mark);
1207             return Token.init;
1208         }
1209 
1210         /// Scan a block scalar token with specified style.
1211         ///
1212         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1213         Token scanBlockScalar(const ScalarStyle style) @trusted
1214         {
1215             const startMark = reader_.mark;
1216 
1217             // Scan the header.
1218             reader_.forward();
1219 
1220             const indicators = scanBlockScalarIndicators(startMark);
1221             if(error_) { return Token.init; }
1222 
1223             const chomping   = indicators[0];
1224             const increment  = indicators[1];
1225             scanBlockScalarIgnoredLine(startMark);
1226             if(error_) { return Token.init; }
1227 
1228             // Determine the indentation level and go to the first non-empty line.
1229             Mark endMark;
1230             uint indent = max(1, indent_ + 1);
1231 
1232             reader_.sliceBuilder.begin();
1233             alias Transaction = SliceBuilder.Transaction;
1234             // Used to strip the last line breaks written to the slice at the end of the
1235             // scalar, which may be needed based on chomping.
1236             Transaction breaksTransaction = Transaction(reader_.sliceBuilder);
1237             // Read the first indentation/line breaks before the scalar.
1238             size_t startLen = reader_.sliceBuilder.length;
1239             if(increment == int.min)
1240             {
1241                 auto indentation = scanBlockScalarIndentationToSlice();
1242                 endMark = indentation[1];
1243                 indent  = max(indent, indentation[0]);
1244             }
1245             else
1246             {
1247                 indent += increment - 1;
1248                 endMark = scanBlockScalarBreaksToSlice(indent);
1249             }
1250 
1251             // int.max means there's no line break (int.max is outside UTF-32).
1252             dchar lineBreak = cast(dchar)int.max;
1253 
1254             // Scan the inner part of the block scalar.
1255             while(reader_.column == indent && reader_.peekByte() != '\0')
1256             {
1257                 breaksTransaction.commit();
1258                 const bool leadingNonSpace = !" \t"d.canFind(reader_.peekByte());
1259                 // This is where the 'interesting' non-whitespace data gets read.
1260                 scanToNextBreakToSlice();
1261                 lineBreak = scanLineBreak();
1262 
1263 
1264                 // This transaction serves to rollback data read in the
1265                 // scanBlockScalarBreaksToSlice() call.
1266                 breaksTransaction = Transaction(reader_.sliceBuilder);
1267                 startLen = reader_.sliceBuilder.length;
1268                 // The line breaks should actually be written _after_ the if() block
1269                 // below. We work around that by inserting
1270                 endMark = scanBlockScalarBreaksToSlice(indent);
1271 
1272                 // This will not run during the last iteration (see the if() vs the
1273                 // while()), hence breaksTransaction rollback (which happens after this
1274                 // loop) will never roll back data written in this if() block.
1275                 if(reader_.column == indent && reader_.peekByte() != '\0')
1276                 {
1277                     // Unfortunately, folding rules are ambiguous.
1278 
1279                     // This is the folding according to the specification:
1280                     if(style == ScalarStyle.Folded && lineBreak == '\n' &&
1281                        leadingNonSpace && !" \t"d.canFind(reader_.peekByte()))
1282                     {
1283                         // No breaks were scanned; no need to insert the space in the
1284                         // middle of slice.
1285                         if(startLen == reader_.sliceBuilder.length)
1286                         {
1287                             reader_.sliceBuilder.write(' ');
1288                         }
1289                     }
1290                     else
1291                     {
1292                         // We need to insert in the middle of the slice in case any line
1293                         // breaks were scanned.
1294                         reader_.sliceBuilder.insert(lineBreak, startLen);
1295                     }
1296 
1297                     ////this is Clark Evans's interpretation (also in the spec
1298                     ////examples):
1299                     //
1300                     //if(style == ScalarStyle.Folded && lineBreak == '\n')
1301                     //{
1302                     //    if(startLen == endLen)
1303                     //    {
1304                     //        if(!" \t"d.canFind(reader_.peekByte()))
1305                     //        {
1306                     //            reader_.sliceBuilder.write(' ');
1307                     //        }
1308                     //        else
1309                     //        {
1310                     //            chunks ~= lineBreak;
1311                     //        }
1312                     //    }
1313                     //}
1314                     //else
1315                     //{
1316                     //    reader_.sliceBuilder.insertBack(lineBreak, endLen - startLen);
1317                     //}
1318                 }
1319                 else
1320                 {
1321                     break;
1322                 }
1323             }
1324 
1325             // If chompint is Keep, we keep (commit) the last scanned line breaks
1326             // (which are at the end of the scalar). Otherwise re remove them (end the
1327             // transaction).
1328             if(chomping == Chomping.Keep)  { breaksTransaction.commit(); }
1329             else                           { breaksTransaction.__dtor(); }
1330             if(chomping != Chomping.Strip && lineBreak != int.max)
1331             {
1332                 // If chomping is Keep, we keep the line break but the first line break
1333                 // that isn't stripped (since chomping isn't Strip in this branch) must
1334                 // be inserted _before_ the other line breaks.
1335                 if(chomping == Chomping.Keep)
1336                 {
1337                     reader_.sliceBuilder.insert(lineBreak, startLen);
1338                 }
1339                 // If chomping is not Keep, breaksTransaction was cancelled so we can
1340                 // directly write the first line break (as it isn't stripped - chomping
1341                 // is not Strip)
1342                 else
1343                 {
1344                     reader_.sliceBuilder.write(lineBreak);
1345                 }
1346             }
1347 
1348             char[] slice = reader_.sliceBuilder.finish();
1349             return scalarToken(startMark, endMark, slice, style);
1350         }
1351 
1352         /// Scan chomping and indentation indicators of a scalar token.
1353         ///
1354         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1355         Tuple!(Chomping, int) scanBlockScalarIndicators(const Mark startMark) @safe
1356         {
1357             auto chomping = Chomping.Clip;
1358             int increment = int.min;
1359             dchar c       = reader_.peek();
1360 
1361             /// Indicators can be in any order.
1362             if(getChomping(c, chomping))
1363             {
1364                 getIncrement(c, increment, startMark);
1365                 if(error_) { return tuple(Chomping.init, int.max); }
1366             }
1367             else
1368             {
1369                 const gotIncrement = getIncrement(c, increment, startMark);
1370                 if(error_)       { return tuple(Chomping.init, int.max); }
1371                 if(gotIncrement) { getChomping(c, chomping); }
1372             }
1373 
1374             if(" \0\n\r\u0085\u2028\u2029"d.canFind(c))
1375             {
1376                 return tuple(chomping, increment);
1377             }
1378             error("While scanning a block scalar", startMark,
1379                   expected("chomping or indentation indicator", c), reader_.mark);
1380             return tuple(Chomping.init, int.max);
1381         }
1382 
1383         /// Get chomping indicator, if detected. Return false otherwise.
1384         ///
1385         /// Used in scanBlockScalarIndicators.
1386         ///
1387         /// Params:
1388         ///
1389         /// c        = The character that may be a chomping indicator.
1390         /// chomping = Write the chomping value here, if detected.
1391         bool getChomping(ref dchar c, ref Chomping chomping) @safe
1392         {
1393             if(!"+-"d.canFind(c)) { return false; }
1394             chomping = c == '+' ? Chomping.Keep : Chomping.Strip;
1395             reader_.forward();
1396             c = reader_.peek();
1397             return true;
1398         }
1399 
1400         /// Get increment indicator, if detected. Return false otherwise.
1401         ///
1402         /// Used in scanBlockScalarIndicators.
1403         ///
1404         /// Params:
1405         ///
1406         /// c         = The character that may be an increment indicator.
1407         ///             If an increment indicator is detected, this will be updated to
1408         ///             the next character in the Reader.
1409         /// increment = Write the increment value here, if detected.
1410         /// startMark = Mark for error messages.
1411         ///
1412         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1413         bool getIncrement(ref dchar c, ref int increment, const Mark startMark) @safe
1414         {
1415             if(!c.isDigit) { return false; }
1416             // Convert a digit to integer.
1417             increment = c - '0';
1418             assert(increment < 10 && increment >= 0, "Digit has invalid value");
1419             if(increment > 0)
1420             {
1421                 reader_.forward();
1422                 c = reader_.peek();
1423                 return true;
1424             }
1425             error("While scanning a block scalar", startMark,
1426                   expected("indentation indicator in range 1-9", "0"), reader_.mark);
1427             return false;
1428         }
1429 
1430         /// Scan (and ignore) ignored line in a block scalar.
1431         ///
1432         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1433         void scanBlockScalarIgnoredLine(const Mark startMark) @safe
1434         {
1435             findNextNonSpace();
1436             if(reader_.peekByte()== '#') { scanToNextBreak(); }
1437 
1438             if(searchAllBreaks.canFind(reader_.peek()))
1439             {
1440                 scanLineBreak();
1441                 return;
1442             }
1443             error("While scanning a block scalar", startMark,
1444                   expected("comment or line break", reader_.peek()), reader_.mark);
1445         }
1446 
1447         /// Scan indentation in a block scalar, returning line breaks, max indent and end mark.
1448         ///
1449         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1450         /// characters into that slice.
1451         Tuple!(uint, Mark) scanBlockScalarIndentationToSlice() @system
1452         {
1453             uint maxIndent;
1454             Mark endMark = reader_.mark;
1455 
1456             while(" \n\r\u0085\u2028\u2029"d.canFind(reader_.peek()))
1457             {
1458                 if(reader_.peekByte() != ' ')
1459                 {
1460                     reader_.sliceBuilder.write(scanLineBreak());
1461                     endMark = reader_.mark;
1462                     continue;
1463                 }
1464                 reader_.forward();
1465                 maxIndent = max(reader_.column, maxIndent);
1466             }
1467 
1468             return tuple(maxIndent, endMark);
1469         }
1470 
1471         /// Scan line breaks at lower or specified indentation in a block scalar.
1472         ///
1473         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1474         /// characters into that slice.
1475         Mark scanBlockScalarBreaksToSlice(const uint indent) @trusted
1476         {
1477             Mark endMark = reader_.mark;
1478 
1479             for(;;)
1480             {
1481                 while(reader_.column < indent && reader_.peekByte() == ' ') { reader_.forward(); }
1482                 if(!"\n\r\u0085\u2028\u2029"d.canFind(reader_.peek()))  { break; }
1483                 reader_.sliceBuilder.write(scanLineBreak());
1484                 endMark = reader_.mark;
1485             }
1486 
1487             return endMark;
1488         }
1489 
1490         /// Scan a qouted flow scalar token with specified quotes.
1491         ///
1492         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1493         Token scanFlowScalar(const ScalarStyle quotes) @trusted
1494         {
1495             const startMark = reader_.mark;
1496             const quote     = reader_.get();
1497 
1498             reader_.sliceBuilder.begin();
1499             scope(exit) if(error_) { reader_.sliceBuilder.finish(); }
1500 
1501             scanFlowScalarNonSpacesToSlice(quotes, startMark);
1502             if(error_) { return Token.init; }
1503 
1504             while(reader_.peek() != quote)
1505             {
1506                 scanFlowScalarSpacesToSlice(startMark);
1507                 if(error_) { return Token.init; }
1508                 scanFlowScalarNonSpacesToSlice(quotes, startMark);
1509                 if(error_) { return Token.init; }
1510             }
1511             reader_.forward();
1512 
1513             auto slice = reader_.sliceBuilder.finish();
1514             return scalarToken(startMark, reader_.mark, slice, quotes);
1515         }
1516 
1517         /// Scan nonspace characters in a flow scalar.
1518         ///
1519         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1520         /// characters into that slice.
1521         ///
1522         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1523         void scanFlowScalarNonSpacesToSlice(const ScalarStyle quotes, const Mark startMark)
1524             @system
1525         {
1526             for(;;) with(ScalarStyle)
1527             {
1528                 dchar c = reader_.peek();
1529 
1530                 mixin FastCharSearch!" \t\0\n\r\u0085\u2028\u2029\'\"\\"d search;
1531 
1532                 size_t numCodePoints = 0;
1533                 // This is an optimized way of writing:
1534                 // while(!search.canFind(reader_.peek(numCodePoints))) { ++numCodePoints; }
1535                 outer: for(size_t oldSliceLength;;)
1536                 {
1537                     // This will not necessarily make slice 32 chars longer, as not all
1538                     // code points are 1 char.
1539                     const char[] slice = reader_.slice(numCodePoints + 32);
1540                     if(slice.length == oldSliceLength)
1541                     {
1542                         error("While reading a flow scalar", startMark,
1543                               "reached end of file", reader_.mark);
1544                         return;
1545                     }
1546                     for(size_t i = oldSliceLength; i < slice.length;)
1547                     {
1548                         // slice is UTF-8 - need to decode
1549                         const ch = slice[i] < 0x80 ? slice[i++] : decodeValidUTF8NoGC(slice, i);
1550                         if(search.canFind(ch)) { break outer; }
1551                         ++numCodePoints;
1552                     }
1553                     oldSliceLength = slice.length;
1554                 }
1555 
1556                 reader_.sliceBuilder.write(reader_.get(numCodePoints));
1557 
1558                 c = reader_.peek();
1559                 if(quotes == SingleQuoted && c == '\'' && reader_.peek(1) == '\'')
1560                 {
1561                     reader_.forward(2);
1562                     reader_.sliceBuilder.write('\'');
1563                 }
1564                 else if((quotes == DoubleQuoted && c == '\'') ||
1565                         (quotes == SingleQuoted && "\"\\"d.canFind(c)))
1566                 {
1567                     reader_.forward();
1568                     reader_.sliceBuilder.write(c);
1569                 }
1570                 else if(quotes == DoubleQuoted && c == '\\')
1571                 {
1572                     reader_.forward();
1573                     c = reader_.peek();
1574                     if(dyaml.escapes.escapes.canFind(c))
1575                     {
1576                         reader_.forward();
1577                         // Escaping has been moved to Parser as it can't be done in
1578                         // place (in a slice) in case of '\P' and '\L' (very uncommon,
1579                         // but we don't want to break the spec)
1580                         char[2] escapeSequence = ['\\', cast(char)c];
1581                         reader_.sliceBuilder.write(escapeSequence);
1582                     }
1583                     else if(dyaml.escapes.escapeHexCodeList.canFind(c))
1584                     {
1585                         const hexLength = dyaml.escapes.escapeHexLength(c);
1586                         reader_.forward();
1587 
1588                         foreach(i; 0 .. hexLength) if(!reader_.peek(i).isHexDigit)
1589                         {
1590                             error("While scanning a double quoted scalar", startMark,
1591                                   expected("escape sequence of hexadecimal numbers",
1592                                            reader_.peek(i)), reader_.mark);
1593                             return;
1594                         }
1595                         char[] hex = reader_.get(hexLength);
1596                         char[2] escapeStart = ['\\', cast(char) c];
1597                         reader_.sliceBuilder.write(escapeStart);
1598                         reader_.sliceBuilder.write(hex);
1599                         bool overflow;
1600                         // Note: This is just error checking; Parser does the actual
1601                         //       escaping (otherwise we could accidentally create an
1602                         //       escape sequence here that wasn't in input, breaking the
1603                         //       escaping code in parser, which is in parser because it
1604                         //       can't always be done in place)
1605                         parseNoGC!int(hex, 16u, overflow);
1606                         if(overflow)
1607                         {
1608                             error("While scanning a double quoted scalar", startMark,
1609                                   "overflow when parsing an escape sequence of "
1610                                   "hexadecimal numbers.", reader_.mark);
1611                             return;
1612                         }
1613                     }
1614                     else if("\n\r\u0085\u2028\u2029"d.canFind(c))
1615                     {
1616                         scanLineBreak();
1617                         scanFlowScalarBreaksToSlice(startMark);
1618                         if(error_) { return; }
1619                     }
1620                     else
1621                     {
1622                         error("While scanning a double quoted scalar", startMark,
1623                               buildMsg("found unsupported escape character", c),
1624                               reader_.mark);
1625                         return;
1626                     }
1627                 }
1628                 else { return; }
1629             }
1630         }
1631 
1632         /// Scan space characters in a flow scalar.
1633         ///
1634         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1635         /// spaces into that slice.
1636         ///
1637         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1638         void scanFlowScalarSpacesToSlice(const Mark startMark) @system
1639         {
1640             // Increase length as long as we see whitespace.
1641             size_t length = 0;
1642             while(" \t"d.canFind(reader_.peekByte(length))) { ++length; }
1643             auto whitespaces = reader_.prefixBytes(length);
1644 
1645             // Can check the last byte without striding because '\0' is ASCII
1646             const c = reader_.peek(length);
1647             if(c == '\0')
1648             {
1649                 error("While scanning a quoted scalar", startMark,
1650                       "found unexpected end of buffer", reader_.mark);
1651                 return;
1652             }
1653 
1654             // Spaces not followed by a line break.
1655             if(!"\n\r\u0085\u2028\u2029"d.canFind(c))
1656             {
1657                 reader_.forward(length);
1658                 reader_.sliceBuilder.write(whitespaces);
1659                 return;
1660             }
1661 
1662             // There's a line break after the spaces.
1663             reader_.forward(length);
1664             const lineBreak = scanLineBreak();
1665 
1666             if(lineBreak != '\n') { reader_.sliceBuilder.write(lineBreak); }
1667 
1668             // If we have extra line breaks after the first, scan them into the
1669             // slice.
1670             const bool extraBreaks = scanFlowScalarBreaksToSlice(startMark);
1671             if(error_) { return; }
1672 
1673             // No extra breaks, one normal line break. Replace it with a space.
1674             if(lineBreak == '\n' && !extraBreaks) { reader_.sliceBuilder.write(' '); }
1675         }
1676 
1677         /// Scan line breaks in a flow scalar.
1678         ///
1679         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1680         /// line breaks into that slice.
1681         ///
1682         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1683         bool scanFlowScalarBreaksToSlice(const Mark startMark) @system
1684         {
1685             // True if at least one line break was found.
1686             bool anyBreaks;
1687             for(;;)
1688             {
1689                 // Instead of checking indentation, we check for document separators.
1690                 const prefix = reader_.prefix(3);
1691                 if((prefix == "---" || prefix == "...") &&
1692                    searchAllWhitespace.canFind(reader_.peek(3)))
1693                 {
1694                     error("While scanning a quoted scalar", startMark,
1695                           "found unexpected document separator", reader_.mark);
1696                     return false;
1697                 }
1698 
1699                 // Skip any whitespaces.
1700                 while(" \t"d.canFind(reader_.peekByte())) { reader_.forward(); }
1701 
1702                 // Encountered a non-whitespace non-linebreak character, so we're done.
1703                 if(!"\n\r\u0085\u2028\u2029"d.canFind(reader_.peek())) { break; }
1704 
1705                 const lineBreak = scanLineBreak();
1706                 anyBreaks = true;
1707                 reader_.sliceBuilder.write(lineBreak);
1708             }
1709             return anyBreaks;
1710         }
1711 
1712         /// Scan plain scalar token (no block, no quotes).
1713         ///
1714         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1715         Token scanPlain() @trusted
1716         {
1717             // We keep track of the allowSimpleKey_ flag here.
1718             // Indentation rules are loosed for the flow context
1719             const startMark = reader_.mark;
1720             Mark endMark = startMark;
1721             const indent = indent_ + 1;
1722 
1723             // We allow zero indentation for scalars, but then we need to check for
1724             // document separators at the beginning of the line.
1725             // if(indent == 0) { indent = 1; }
1726 
1727             reader_.sliceBuilder.begin();
1728 
1729             alias Transaction = SliceBuilder.Transaction;
1730             Transaction spacesTransaction;
1731             // Stop at a comment.
1732             while(reader_.peekByte() != '#')
1733             {
1734                 // Scan the entire plain scalar.
1735                 size_t length = 0;
1736                 dchar c = void;
1737                 // Moved the if() out of the loop for optimization.
1738                 if(flowLevel_ == 0)
1739                 {
1740                     c = reader_.peek(length);
1741                     for(;;)
1742                     {
1743                         const cNext = reader_.peek(length + 1);
1744                         if(searchAllWhitespace.canFind(c) ||
1745                            (c == ':' && searchAllWhitespace.canFind(cNext)))
1746                         {
1747                             break;
1748                         }
1749                         ++length;
1750                         c = cNext;
1751                     }
1752                 }
1753                 else
1754                 {
1755                     for(;;)
1756                     {
1757                         c = reader_.peek(length);
1758                         if(searchAllWhitespace.canFind(c) || ",:?[]{}"d.canFind(c))
1759                         {
1760                             break;
1761                         }
1762                         ++length;
1763                     }
1764                 }
1765 
1766                 // It's not clear what we should do with ':' in the flow context.
1767                 if(flowLevel_ > 0 && c == ':' &&
1768                    !searchAllWhitespace.canFind(reader_.peek(length + 1)) &&
1769                    !",[]{}"d.canFind(reader_.peek(length + 1)))
1770                 {
1771                     // This is an error; throw the slice away.
1772                     spacesTransaction.commit();
1773                     reader_.sliceBuilder.finish();
1774                     reader_.forward(length);
1775                     error("While scanning a plain scalar", startMark,
1776                           "found unexpected ':' . Please check "
1777                           "http://pyyaml.org/wiki/YAMLColonInFlowContext for details.",
1778                           reader_.mark);
1779                     return Token.init;
1780                 }
1781 
1782                 if(length == 0) { break; }
1783 
1784                 allowSimpleKey_ = false;
1785 
1786                 reader_.sliceBuilder.write(reader_.get(length));
1787 
1788                 endMark = reader_.mark;
1789 
1790                 spacesTransaction.commit();
1791                 spacesTransaction = Transaction(reader_.sliceBuilder);
1792 
1793                 const startLength = reader_.sliceBuilder.length;
1794                 scanPlainSpacesToSlice(startMark);
1795                 if(startLength == reader_.sliceBuilder.length ||
1796                    (flowLevel_ == 0 && reader_.column < indent))
1797                 {
1798                     break;
1799                 }
1800             }
1801 
1802             spacesTransaction.__dtor();
1803             char[] slice = reader_.sliceBuilder.finish();
1804 
1805             return scalarToken(startMark, endMark, slice, ScalarStyle.Plain);
1806         }
1807 
1808         /// Scan spaces in a plain scalar.
1809         ///
1810         /// Assumes that the caller is building a slice in Reader, and puts the spaces
1811         /// into that slice.
1812         void scanPlainSpacesToSlice(const Mark startMark) @system
1813         {
1814             // The specification is really confusing about tabs in plain scalars.
1815             // We just forbid them completely. Do not use tabs in YAML!
1816 
1817             // Get as many plain spaces as there are.
1818             size_t length = 0;
1819             while(reader_.peekByte(length) == ' ') { ++length; }
1820             char[] whitespaces = reader_.prefixBytes(length);
1821             reader_.forward(length);
1822 
1823             dchar c = reader_.peek();
1824             mixin FastCharSearch!" \n\r\u0085\u2028\u2029"d search;
1825             // No newline after the spaces (if any)
1826             // (Excluding ' ' so we can use the same FastCharSearch as below)
1827             if(!search.canFind(c) && c != ' ')
1828             {
1829                 // We have spaces, but no newline.
1830                 if(whitespaces.length > 0) { reader_.sliceBuilder.write(whitespaces); }
1831                 return;
1832             }
1833 
1834             // Newline after the spaces (if any)
1835             const lineBreak = scanLineBreak();
1836             allowSimpleKey_ = true;
1837 
1838             static bool end(Reader reader_) @safe pure nothrow @nogc
1839             {
1840                 const prefix = reader_.prefix(3);
1841                 return ("---" == prefix || "..." == prefix)
1842                         && " \t\0\n\r\u0085\u2028\u2029"d.canFind(reader_.peek(3));
1843             }
1844 
1845             if(end(reader_)) { return; }
1846 
1847             bool extraBreaks = false;
1848 
1849             alias Transaction = SliceBuilder.Transaction;
1850             auto transaction = Transaction(reader_.sliceBuilder);
1851             if(lineBreak != '\n') { reader_.sliceBuilder.write(lineBreak); }
1852             while(search.canFind(reader_.peek()))
1853             {
1854                 if(reader_.peekByte() == ' ') { reader_.forward(); }
1855                 else
1856                 {
1857                     const lBreak = scanLineBreak();
1858                     extraBreaks  = true;
1859                     reader_.sliceBuilder.write(lBreak);
1860 
1861                     if(end(reader_)) { return; }
1862                 }
1863             }
1864             transaction.commit();
1865 
1866             // No line breaks, only a space.
1867             if(lineBreak == '\n' && !extraBreaks) { reader_.sliceBuilder.write(' '); }
1868         }
1869 
1870         /// Scan handle of a tag token.
1871         ///
1872         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1873         /// characters into that slice.
1874         ///
1875         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1876         void scanTagHandleToSlice(string name)(const Mark startMark) @system
1877         {
1878             dchar c = reader_.peek();
1879             enum contextMsg = "While scanning a " ~ name;
1880             if(c != '!')
1881             {
1882                 error(contextMsg, startMark, expected("'!'", c), reader_.mark);
1883                 return;
1884             }
1885 
1886             uint length = 1;
1887             c = reader_.peek(length);
1888             if(c != ' ')
1889             {
1890                 while(c.isAlphaNum || "-_"d.canFind(c))
1891                 {
1892                     ++length;
1893                     c = reader_.peek(length);
1894                 }
1895                 if(c != '!')
1896                 {
1897                     reader_.forward(length);
1898                     error(contextMsg, startMark, expected("'!'", c), reader_.mark);
1899                     return;
1900                 }
1901                 ++length;
1902             }
1903 
1904             reader_.sliceBuilder.write(reader_.get(length));
1905         }
1906 
1907         /// Scan URI in a tag token.
1908         ///
1909         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1910         /// characters into that slice.
1911         ///
1912         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1913         void scanTagURIToSlice(string name)(const Mark startMark) @trusted
1914         {
1915             // Note: we do not check if URI is well-formed.
1916             dchar c = reader_.peek();
1917             const startLen = reader_.sliceBuilder.length;
1918             {
1919                 uint length = 0;
1920                 mixin FastCharSearch!"-;/?:@&=+$,_.!~*\'()[]%"d search;
1921                 while(c.isAlphaNum || search.canFind(c))
1922                 {
1923                     if(c == '%')
1924                     {
1925                         auto chars = reader_.get(length);
1926                         reader_.sliceBuilder.write(chars);
1927                         length = 0;
1928                         scanURIEscapesToSlice!name(startMark);
1929                         if(error_) { return; }
1930                     }
1931                     else { ++length; }
1932                     c = reader_.peek(length);
1933                 }
1934                 if(length > 0)
1935                 {
1936                     auto chars = reader_.get(length);
1937                     reader_.sliceBuilder.write(chars);
1938                     length = 0;
1939                 }
1940             }
1941             // OK if we scanned something, error otherwise.
1942             if(reader_.sliceBuilder.length > startLen) { return; }
1943 
1944             enum contextMsg = "While parsing a " ~ name;
1945             error(contextMsg, startMark, expected("URI", c), reader_.mark);
1946         }
1947 
1948         // Not @nogc yet because std.utf.decode is not @nogc
1949         /// Scan URI escape sequences.
1950         ///
1951         /// Assumes that the caller is building a slice in Reader, and puts the scanned
1952         /// characters into that slice.
1953         ///
1954         /// In case of an error, error_ is set. Use throwIfError() to handle this.
1955         void scanURIEscapesToSlice(string name)(const Mark startMark) @system
1956         {
1957             // URI escapes encode a UTF-8 string. We store UTF-8 code units here for
1958             // decoding into UTF-32.
1959             char[4] bytes;
1960             size_t bytesUsed;
1961             Mark mark = reader_.mark;
1962 
1963             // Get one dchar by decoding data from bytes.
1964             //
1965             // This is probably slow, but simple and URI escapes are extremely uncommon
1966             // in YAML.
1967             //
1968             // Returns the number of bytes used by the dchar in bytes on success,
1969             // size_t.max on failure.
1970             static size_t getDchar(char[] bytes, Reader reader_)
1971             {
1972                 size_t nextChar;
1973                 dchar c;
1974                 if(bytes[0] < 0x80)
1975                 {
1976                     c = bytes[0];
1977                     ++nextChar;
1978                 }
1979                 else
1980                 {
1981                     const decoded = decodeUTF8NoGC!(No.validated)(bytes[], nextChar);
1982                     if(decoded.errorMessage !is null) { return size_t.max; }
1983                     c = decoded.decoded;
1984                 }
1985                 reader_.sliceBuilder.write(c);
1986                 if(bytes.length - nextChar > 0)
1987                 {
1988                     core.stdc..string.memmove(bytes.ptr, bytes.ptr + nextChar,
1989                                              bytes.length - nextChar);
1990                 }
1991                 return bytes.length - nextChar;
1992             }
1993 
1994             enum contextMsg = "While scanning a " ~ name;
1995             while(reader_.peekByte() == '%')
1996             {
1997                 reader_.forward();
1998                 if(bytesUsed == bytes.length)
1999                 {
2000                     bytesUsed = getDchar(bytes[], reader_);
2001                     if(bytesUsed == size_t.max)
2002                     {
2003                         error(contextMsg, startMark,
2004                                 "Invalid UTF-8 data encoded in URI escape sequence",
2005                                 reader_.mark);
2006                         return;
2007                     }
2008                 }
2009 
2010                 char b = 0;
2011                 uint mult = 16;
2012                 // Converting 2 hexadecimal digits to a byte.
2013                 foreach(k; 0 .. 2)
2014                 {
2015                     const dchar c = reader_.peek(k);
2016                     if(!c.isHexDigit)
2017                     {
2018                         auto msg = expected("URI escape sequence of 2 hexadecimal "
2019                                             "numbers", c);
2020                         error(contextMsg, startMark, msg, reader_.mark);
2021                         return;
2022                     }
2023 
2024                     uint digit;
2025                     if(c - '0' < 10)     { digit = c - '0'; }
2026                     else if(c - 'A' < 6) { digit = c - 'A'; }
2027                     else if(c - 'a' < 6) { digit = c - 'a'; }
2028                     else                 { assert(false); }
2029                     b += mult * digit;
2030                     mult /= 16;
2031                 }
2032                 bytes[bytesUsed++] = b;
2033 
2034                 reader_.forward(2);
2035             }
2036 
2037             bytesUsed = getDchar(bytes[0 .. bytesUsed], reader_);
2038         }
2039 
2040 
2041         /// Scan a line break, if any.
2042         ///
2043         /// Transforms:
2044         ///   '\r\n'      :   '\n'
2045         ///   '\r'        :   '\n'
2046         ///   '\n'        :   '\n'
2047         ///   '\u0085'    :   '\n'
2048         ///   '\u2028'    :   '\u2028'
2049         ///   '\u2029     :   '\u2029'
2050         ///   no break    :   '\0'
2051         dchar scanLineBreak() @safe
2052         {
2053             // Fast path for ASCII line breaks.
2054             const b = reader_.peekByte();
2055             if(b < 0x80)
2056             {
2057                 if(b == '\n' || b == '\r')
2058                 {
2059                     if(reader_.prefix(2) == "\r\n") { reader_.forward(2); }
2060                     else { reader_.forward(); }
2061                     return '\n';
2062                 }
2063                 return '\0';
2064             }
2065 
2066             const c = reader_.peek();
2067             if(c == '\x85')
2068             {
2069                 reader_.forward();
2070                 return '\n';
2071             }
2072             if(c == '\u2028' || c == '\u2029')
2073             {
2074                 reader_.forward();
2075                 return c;
2076             }
2077             return '\0';
2078         }
2079 }
2080 
2081 private:
2082 
2083 /// A nothrow function that converts a dchar[] to a string.
2084 string utf32To8(C)(C[] str) @safe pure nothrow
2085     if(is(Unqual!C == dchar))
2086 {
2087     try                    { return str.to!string; }
2088     catch(ConvException e) { assert(false, "Unexpected invalid UTF-32 string"); }
2089     catch(Exception e)     { assert(false, "Unexpected exception during UTF-8 encoding"); }
2090 }