Ast.Variant _GetVariant(FtlParserStream ps, bool hasDefault) { bool defaultIndex = false; if (ps.CurrentIs('*')) { if (hasDefault) { throw new ParseException("E0015"); } ps.Next(); defaultIndex = true; hasDefault = true; } ps.ExpectChar('['); var key = GetVariantKey(ps); ps.ExpectChar(']'); if (ps.IsPeekValueStart()) { ps.SkipIndent(); var value = GetValue(ps); return(new Ast.Variant(key, value, defaultIndex)); } throw new ParseException("E0012"); }
Ast.Identifier _GetTermIdentifier(FtlParserStream ps) { ps.ExpectChar('-'); var id = this.GetIdentifier(ps); return(new Ast.Identifier($"-{id.Name}")); }
Ast.Entry GetEntryOrJunk(FtlParserStream ps) { var entryStartPos = ps.GetPosition(); ps.BeginCapture(); try { var entry = GetEntry(ps); ps.ExpectNewLine(); return(entry); } catch (ParseException e) { var errorPos = ps.GetPosition(); ps.SkipToNextEntryStart(); var nextEntryStart = ps.GetPosition(); // Create a Junk instance var junk = new Ast.Junk(ps.GetCapturedText()); if (_withSpans) { junk.AddSpan(entryStartPos, nextEntryStart); } var annot = new Ast.Annotation(e.Code, e.Args, e.Message); annot.AddSpan(errorPos, errorPos); junk.AddAnnotation(annot); return(junk); } }
IReadOnlyList <Ast.Variant> GetVariants(FtlParserStream ps) { var variants = new List <Ast.Variant>(); bool hasDefault = false; while (true) { ps.ExpectIndent(); var variant = GetVariant(ps, hasDefault); if (variant.IsDefault) { hasDefault = true; } variants.Add(variant); if (!ps.IsPeekNextLineVariantStart()) { break; } } if (!hasDefault) { throw new ParseException("E0010"); } return(variants); }
Ast.Entry _GetMessage(FtlParserStream ps) { var id = GetIdentifier(ps); ps.SkipInlineWs(); ps.ExpectChar('='); Ast.Pattern pattern = null; if (ps.IsPeekValueStart()) { ps.SkipIndent(); pattern = GetPattern(ps); } else { ps.SkipInlineWs(); } IReadOnlyList <Ast.Attribute> attrs = null; if (ps.IsPeekNextLineAttributeStart()) { attrs = GetAttributes(ps); } if (pattern == null && attrs == null) { throw new ParseException("E0005", id.Name); } return(new Ast.Message(id, pattern, attrs)); }
Ast.Entry _GetTerm(FtlParserStream ps) { var id = GetTermIdentifier(ps); ps.SkipInlineWs(); ps.ExpectChar('='); Ast.SyntaxNode value = null; if (ps.IsPeekValueStart()) { ps.SkipIndent(); value = GetValue(ps); } else { throw new ParseException("E0006", id.Name); } IReadOnlyList <Ast.Attribute> attrs = null; if (ps.IsPeekNextLineAttributeStart()) { attrs = GetAttributes(ps); } return(new Ast.Term(id, value, attrs)); }
Ast.StringLiteral _GetString(FtlParserStream ps) { var val = new StringBuilder(); ps.ExpectChar('"'); int ch; while ((ch = ps.TakeChar(x => x != '"' && x != '\r' && x != '\n')) != Eof) { if (ch == '\\') { GetEscapeSequence(ps, new int[] { '{', '\\', '"' }, val); } else { val.Append((char)ch); } } if (ps.CurrentIs('\r') || ps.CurrentIs('\n')) { throw new ParseException("E0020"); } ps.Next(); return(new Ast.StringLiteral(val.ToString())); }
Ast.SyntaxNode _GetCallArg(FtlParserStream ps) { var exp = GetSelectorExpression(ps); ps.SkipInlineWs(); if (ps.Current != ':') { return(exp); } var messageReference = exp as Ast.MessageReference; if (messageReference == null) { throw new ParseException("E0009"); } ps.Next(); ps.SkipInlineWs(); var val = GetArgVal(ps); return(new Ast.NamedArgument(messageReference.Id, val)); }
public Ast.Resource Parse(TextReader input) { var ps = new FtlParserStream(input); ps.SkipBlankLines(); var entries = new List <Ast.Entry>(); Ast.Comment lastComment = null; while (ps.Current != Eof) { var entry = GetEntryOrJunk(ps); int blankLines = ps.SkipBlankLines(); // Regular Comments require special logic. Comments may be attached to // Messages or Terms if they are followed immediately by them. However // they should parse as standalone when they're followed by Junk. // Consequently, we only attach Comments once we know that the Message // or the Term parsed successfully. if (entry is Ast.Comment comment && blankLines == 0 && ps.Current != Eof) { // Stash the comment and decide what to do with it in the next pass. lastComment = comment; continue; } if (lastComment != null) { if (entry is Ast.MessageTermBase mt) { mt.Comment = lastComment; if (_withSpans) { mt.Span.Start = lastComment.Span.Start; } } else { entries.Add(lastComment); } // In either case, the stashed comment has been dealt with; clear it. lastComment = null; } // No special logic for other types of entries. entries.Add(entry); } var res = new Ast.Resource(entries); if (_withSpans) { res.AddSpan(Position.Start, ps.GetPosition()); } return(res); }
Ast.Placeable _GetPlaceable(FtlParserStream ps) { ps.ExpectChar('{'); var expression = GetExpression(ps); ps.ExpectChar('}'); return(new Ast.Placeable(expression)); }
public Ast.BaseComment _GetComment(FtlParserStream ps) { // 0 - comment // 1 - group comment // 2 - resource comment int level = -1; var content = new StringBuilder(); while (true) { int i = -1; while (ps.CurrentIs('#') && (i < (level == -1 ? 2 : level))) { ps.Next(); i++; } if (level == -1) { level = i; } if (!ps.IsPeekNewLine()) { ps.ExpectChar(' '); int ch; while ((ch = ps.TakeChar(x => x != '\r' && x != '\n')) != Eof) { content.Append((char)ch); } } if (ps.IsPeekNextLineComment(level)) { content.Append('\n'); ps.SkipNewLine(); } else { break; } } var text = content.ToString(); switch (level) { case 0: return(new Ast.Comment(text)); case 1: return(new Ast.GroupComment(text)); case 2: return(new Ast.ResourceComment(text)); } throw new InvalidOperationException($"Unknown level value '{level}'"); }
public static string TrimRight(string s) { int end = s.Length; for (; end > 0 && FtlParserStream.IsWhite(s[end - 1]); --end) { } return(s.Length == end ? s : s.Substring(0, end)); }
Ast.SyntaxNode _GetVariantList(FtlParserStream ps) { ps.ExpectChar('{'); ps.SkipInlineWs(); var variants = GetVariants(ps); ps.ExpectIndent(); ps.ExpectChar('}'); return(new Ast.VariantList(variants)); }
Ast.Expression GetArgVal(FtlParserStream ps) { if (ps.IsNumberStart()) { return(GetNumber(ps)); } else if (ps.CurrentIs('"')) { return(GetString(ps)); } throw new ParseException("E0012"); }
Ast.SyntaxNode _GetValue(FtlParserStream ps) { if (ps.CurrentIs('{')) { ps.Peek(); ps.PeekInlineWs(); if (ps.IsPeekNextLineVariantStart()) { return(GetVariantList(ps)); } } return(GetPattern(ps)); }
CallArgs GetCallArgs(FtlParserStream ps) { var result = new CallArgs(); var argumentNames = new HashSet <string>(); ps.SkipInlineWs(); ps.SkipIndent(); while (true) { if (ps.Current == ')') { break; } var arg = GetCallArg(ps); if (arg is Ast.NamedArgument narg) { if (argumentNames.Contains(narg.Name.Name)) { throw new ParseException("E0022"); } result.Named.Add(narg); argumentNames.Add(narg.Name.Name); } else if (argumentNames.Count > 0) { throw new ParseException("E0021"); } else { result.Positional.Add(arg); } ps.SkipInlineWs(); ps.SkipIndent(); if (ps.Current == ',') { ps.Next(); ps.SkipInlineWs(); ps.SkipIndent(); continue; } else { break; } } return(result); }
Ast.Identifier _GetIdentifier(FtlParserStream ps) { var name = new StringBuilder(); name.Append((char)ps.TakeIDStart()); int ch; while ((ch = ps.TakeIDChar()) != Eof) { name.Append((char)ch); } return(new Ast.Identifier(name.ToString())); }
Ast.SyntaxNode GetVariantKey(FtlParserStream ps) { var ch = ps.Current; if (ch == Eof) { throw new ParseException("E0013"); } if ((ch >= '0' && ch <= '9') || ch == '-') { return(GetNumber(ps)); } return(GetVariantName(ps)); }
IReadOnlyList <Ast.Attribute> GetAttributes(FtlParserStream ps) { var attrs = new List <Ast.Attribute>(); while (true) { ps.ExpectIndent(); var attr = GetAttribute(ps); attrs.Add(attr); if (!ps.IsPeekNextLineAttributeStart()) { break; } } return(attrs); }
string GetDigits(FtlParserStream ps) { var num = new StringBuilder(); int ch; while ((ch = ps.TakeDigit()) != Eof) { num.Append((char)ch); } if (num.Length == 0) { throw new ParseException("E0004", "0-9"); } return(num.ToString()); }
Ast.Attribute _GetAttribute(FtlParserStream ps) { ps.ExpectChar('.'); var key = GetIdentifier(ps); ps.SkipInlineWs(); ps.ExpectChar('='); if (ps.IsPeekValueStart()) { ps.SkipIndent(); var value = GetPattern(ps); return(new Ast.Attribute(key, value)); } throw new ParseException("E0012"); }
public Ast.Entry GetEntry(FtlParserStream ps) { if (ps.CurrentIs('#')) { return(GetComment(ps)); } if (ps.CurrentIs('-')) { return(GetTerm(ps)); } if (ps.IsIdentifierStart()) { return(GetMessage(ps)); } throw new ParseException("E0002"); }
/// <summary> /// Parse the first Message or Term in `source`. /// /// Skip all encountered comments and start parsing at the first Message or /// Term start. Return Junk if the parsing is not successful. /// /// Preceding comments are ignored unless they contain syntax errors /// themselves, in which case Junk for the invalid comment is returned. /// </summary> public Ast.Entry ParseEntry(TextReader source) { var ps = new FtlParserStream(source); ps.SkipBlankLines(); while (ps.CurrentIs('#')) { var skipped = GetEntryOrJunk(ps); if (skipped is Ast.Junk) { // Don't skip Junk comments. return(skipped); } ps.SkipBlankLines(); } return(GetEntryOrJunk(ps)); }
Ast.TextElement _GetTextElement(FtlParserStream ps) { var buffer = new StringBuilder(); int ch; while ((ch = ps.Current) != Eof) { if (ch == '{') { return(new Ast.TextElement(buffer.ToString())); } if (ch == '\r' || ch == '\n') { if (!ps.IsPeekNextLineValue()) { return(new Ast.TextElement(buffer.ToString())); } ps.SkipNewLine(); ps.SkipInlineWs(); // Add the new line to the buffer buffer.Append((char)ch); continue; } if (ch == '\\') { ps.Next(); GetEscapeSequence(ps, new int[] { '{', '\\' }, buffer); continue; } buffer.Append((char)ps.Current); ps.Next(); } return(new Ast.TextElement(buffer.ToString())); }
Ast.VariantName _GetVariantName(FtlParserStream ps) { var name = new StringBuilder(); name.Append((char)ps.TakeIDStart()); while (true) { var ch = ps.TakeVariantNameChar(); if (ch != Eof) { name.Append((char)ch); } else { break; } } return(new Ast.VariantName(TrimRight(name.ToString()))); }
Ast.NumberLiteral _GetNumber(FtlParserStream ps) { var num = new StringBuilder(); if (ps.CurrentIs('-')) { num.Append('-'); ps.Next(); } num.Append(GetDigits(ps)); if (ps.CurrentIs('.')) { num.Append('.'); ps.Next(); num.Append(GetDigits(ps)); } return(new Ast.NumberLiteral(num.ToString())); }
Ast.Expression _GetLiteral(FtlParserStream ps) { var ch = ps.Current; if (ch == Eof) { throw new ParseException("E0014"); } if (ch == '$') { ps.Next(); var id = GetIdentifier(ps); return(new Ast.VariableReference(id)); } if (ps.IsIdentifierStart()) { var name = GetIdentifier(ps); return(new Ast.MessageReference(name)); } if (ps.IsNumberStart()) { return(GetNumber(ps)); } if (ch == '-') { var id = GetTermIdentifier(ps); return(new Ast.TermReference(id)); } if (ch == '"') { return(GetString(ps)); } throw new ParseException("E0014"); }
T SpanWrapper <T>(FtlParserStream ps, Func <T> wrappedFn) where T : Ast.SyntaxNode { if (!_withSpans) { return(wrappedFn()); } var start = ps.GetPosition(); var node = wrappedFn(); // Don't re-add the span if the node already has it. This may happen when // one decorated function calls another decorated function. if (node.Span != null) { return(node); } var end = ps.GetPosition(); node.AddSpan(start, end); return(node); }
Ast.Pattern _GetPattern(FtlParserStream ps) { var elements = new List <Ast.SyntaxNode>(); ps.SkipInlineWs(); int ch; while ((ch = ps.Current) != Eof) { // The end condition for GetPattern's while loop is a newline // which is not followed by a valid pattern continuation. if (ps.IsPeekNewLine() && !ps.IsPeekNextLineValue()) { break; } if (ch == '{') { var element = GetPlaceable(ps); elements.Add(element); } else { var element = GetTextElement(ps); elements.Add(element); } } // Trim trailing whitespace. if (elements.Count > 0 && elements[elements.Count - 1] is Ast.TextElement te) { te.Value = TrimRight(te.Value); } return(new Ast.Pattern(elements)); }
void GetEscapeSequence(FtlParserStream ps, int[] specials, StringBuilder buffer) { int next = ps.Current; if (Array.IndexOf(specials, next) >= 0) { ps.Next(); buffer.Append('\\').Append((char)next); return; } if (next == 'u') { ps.Next(); char[] sequence = new char[4]; for (int i = 0; i < 4; ++i) { int ch = ps.TakeHexDigit(); if (ch == Eof) { var msg = new String(sequence, 0, i); if (ps.Current != Eof) { msg += (char)ps.Current; } throw new ParseException("E0026", msg); } sequence[i] = (char)ch; } buffer.Append("\\u").Append(sequence); return; } throw new ParseException("E0025", next == Eof ? "" : ((char)next).ToString()); }