├── FluiParser
├── Language
│ ├── Syntax
│ │ ├── WidgetKind.cs
│ │ ├── SyntaxCategory.cs
│ │ ├── SyntaxKind.cs
│ │ ├── Nodes
│ │ │ ├── CallbackNode.cs
│ │ │ ├── IdentifierNode.cs
│ │ │ ├── FunctionCallNode.cs
│ │ │ ├── NodeWithValue.cs
│ │ │ ├── ElementNode.cs
│ │ │ ├── ElementSingleNode.cs
│ │ │ ├── AttributeNode.cs
│ │ │ ├── AttributeSingleNode.cs
│ │ │ └── ConstantNode.cs
│ │ ├── SyntaxNode.cs
│ │ └── SourceDocument.cs
│ ├── TokenCategory.cs
│ ├── Parser
│ │ ├── SyntaxException.cs
│ │ ├── ParserOptions.cs
│ │ └── Parser.cs
│ ├── Generator
│ │ ├── GeneratorOptions.cs
│ │ ├── Generator.cs
│ │ ├── Generator.ViewModel.cs
│ │ └── Generator.View.cs
│ ├── TokenKind.cs
│ ├── SourceLocation.cs
│ ├── SourceSpan.cs
│ ├── ErrorSink.cs
│ ├── Token.cs
│ ├── SourceCode.cs
│ └── Tokenizer
│ │ └── Tokenizer.cs
├── FluiParser.csproj
├── sample.flui
├── Utility
│ └── ExtensionMethods.cs
├── schema.ebnf
└── Program.cs
├── FluiParser.sln
├── .gitignore
└── README.md
/FluiParser/Language/Syntax/WidgetKind.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax
6 | {
7 | public enum WidgetKind
8 | {
9 | Invalid,
10 | Stateless,
11 | Stateful,
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/SyntaxCategory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax
6 | {
7 | public enum SyntaxCategory
8 | {
9 | Invalid,
10 | Metadata,
11 | Element,
12 | Attribute,
13 | Value
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FluiParser/Language/TokenCategory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language
6 | {
7 | public enum TokenCategory
8 | {
9 | Unknown,
10 | WhiteSpace,
11 | Comment,
12 |
13 | Constant,
14 | Identifier,
15 | Punctuation,
16 |
17 | Metadata,
18 |
19 | Invalid,
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/SyntaxKind.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax
6 | {
7 | public enum SyntaxKind
8 | {
9 | Invalid,
10 | SourceDocument,
11 | ElementNode,
12 | AttributeNode,
13 | IdentifierNode,
14 | FunctionCallNode,
15 | ConstantNode,
16 | AttributeSingleNode,
17 | ElementSingleNode,
18 | CallbackNode,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/FluiParser/FluiParser.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/CallbackNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class CallbackNode : NodeWithValue
8 | {
9 | public override SyntaxKind Kind => SyntaxKind.CallbackNode;
10 | public override SyntaxCategory Category => SyntaxCategory.Value;
11 |
12 | public CallbackNode(SourceSpan span, string value)
13 | : base(span, value) { }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FluiParser/sample.flui:
--------------------------------------------------------------------------------
1 | Stateless .viewModel SampleViewModel, .view SampleView:
2 | Center .child:
3 | Column .children:
4 | ! Child 1
5 | Card .child:
6 | Column .children:
7 | Text 'Hello World'
8 | Text 'Flui says hello'
9 | ! Child 2
10 | Card .child:
11 | Column .children:
12 | Text 'Hello again, World', .style textStyle
13 | Text:
14 | 'Flui wants a bagel'
15 | .style $getStyle
16 | RaisedButton:
17 | .title 'Give Flui a bagel'
18 | .onPressed @giveBagel
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/IdentifierNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class IdentifierNode : NodeWithValue
8 | {
9 | public override SyntaxKind Kind => SyntaxKind.IdentifierNode;
10 | public override SyntaxCategory Category => SyntaxCategory.Value;
11 |
12 | public IdentifierNode(SourceSpan span, string value)
13 | : base(span, value) { }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/FunctionCallNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class FunctionCallNode : NodeWithValue
8 | {
9 | public override SyntaxKind Kind => SyntaxKind.FunctionCallNode;
10 | public override SyntaxCategory Category => SyntaxCategory.Value;
11 |
12 | public FunctionCallNode(SourceSpan span, string value)
13 | : base(span, value) { }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FluiParser/Language/Parser/SyntaxException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Parser
6 | {
7 | [Serializable]
8 | public class SyntaxException : Exception
9 | {
10 | public SyntaxException() { }
11 | public SyntaxException(string message) { }
12 | public SyntaxException(string message, Exception inner) { }
13 |
14 | protected SyntaxException(
15 | System.Runtime.Serialization.SerializationInfo info,
16 | System.Runtime.Serialization.StreamingContext context)
17 | : base(info, context) { }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/NodeWithValue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public abstract class NodeWithValue : SyntaxNode
8 | {
9 | [Newtonsoft.Json.JsonProperty(Order = -3)]
10 | public string Value { get; }
11 |
12 | protected NodeWithValue(SourceSpan span, string value)
13 | : base(span)
14 | {
15 | Value = value;
16 | }
17 |
18 | public override string ToString()
19 | {
20 | return $"Category: {Category}, Kind: {Kind}, Value: \"{Value}\"";
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/ElementNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class ElementNode : NodeWithValue
8 | {
9 | [Newtonsoft.Json.JsonProperty(Order = 10)]
10 | public SyntaxNode[] Children { get; }
11 |
12 | public override SyntaxKind Kind => SyntaxKind.ElementNode;
13 | public override SyntaxCategory Category => SyntaxCategory.Element;
14 |
15 | public ElementNode(SourceSpan span, string value, SyntaxNode[] children)
16 | : base(span, value)
17 | {
18 | Children = children;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/ElementSingleNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class ElementSingleNode : NodeWithValue
8 | {
9 | [Newtonsoft.Json.JsonProperty(Order = 10)]
10 | public SyntaxNode Child { get; }
11 |
12 | public override SyntaxKind Kind => SyntaxKind.ElementSingleNode;
13 | public override SyntaxCategory Category => SyntaxCategory.Element;
14 |
15 | public ElementSingleNode(SourceSpan span, string value, SyntaxNode child)
16 | : base(span, value)
17 | {
18 | Child = child;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/AttributeNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class AttributeNode : NodeWithValue
8 | {
9 | [Newtonsoft.Json.JsonProperty(Order = 10)]
10 | public SyntaxNode[] Children { get; }
11 |
12 | public override SyntaxKind Kind => SyntaxKind.AttributeNode;
13 | public override SyntaxCategory Category => SyntaxCategory.Attribute;
14 |
15 | public AttributeNode(SourceSpan span, string value, SyntaxNode[] children)
16 | : base(span, value)
17 | {
18 | Children = children;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/FluiParser/Language/Parser/ParserOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Parser
6 | {
7 | public sealed class ParserOptions
8 | {
9 | public static readonly ParserOptions Default = new ParserOptions();
10 | public static readonly ParserOptions Strict = new ParserOptions() { EnforceStrictIndentation = true, EnforceColons = true };
11 |
12 | public bool EnforceStrictIndentation { get; set; }
13 | public bool EnforceColons { get; set; }
14 |
15 | public ParserOptions()
16 | {
17 | EnforceColons = false;
18 | EnforceStrictIndentation = false;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/AttributeSingleNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class AttributeSingleNode : NodeWithValue
8 | {
9 | [Newtonsoft.Json.JsonProperty(Order = 10)]
10 | public SyntaxNode Child { get; }
11 |
12 | public override SyntaxKind Kind => SyntaxKind.AttributeSingleNode;
13 | public override SyntaxCategory Category => SyntaxCategory.Attribute;
14 |
15 | public AttributeSingleNode(SourceSpan span, string value, SyntaxNode child)
16 | : base(span, value)
17 | {
18 | Child = child;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/FluiParser/Language/Generator/GeneratorOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Generator
6 | {
7 | public sealed class GeneratorOptions
8 | {
9 | public static readonly GeneratorOptions Tabs = new GeneratorOptions() { IndentationCharacter = '\t', IndentationLength = 1 };
10 | public static readonly GeneratorOptions Spaces = new GeneratorOptions() { IndentationCharacter = ' ', IndentationLength = 2 };
11 | public static readonly GeneratorOptions Default = Spaces;
12 |
13 | public char IndentationCharacter { get; set; }
14 | public int IndentationLength { get; set; }
15 |
16 | public GeneratorOptions()
17 | {
18 | IndentationCharacter = '\t';
19 | IndentationLength = 1;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/SyntaxNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax
6 | {
7 | public abstract class SyntaxNode
8 | {
9 | [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
10 | [Newtonsoft.Json.JsonProperty(Order = -5)]
11 | public abstract SyntaxCategory Category { get; }
12 |
13 | [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
14 | [Newtonsoft.Json.JsonProperty(Order = -4)]
15 | public abstract SyntaxKind Kind { get; }
16 |
17 | [Newtonsoft.Json.JsonIgnore]
18 | public SourceSpan Span { get; }
19 |
20 | protected SyntaxNode(SourceSpan span)
21 | {
22 | Span = span;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/FluiParser/Language/Syntax/Nodes/ConstantNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language.Syntax.Nodes
6 | {
7 | public sealed class ConstantNode : NodeWithValue
8 | {
9 | [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
10 | public ConstantKind ConstantKind { get; }
11 |
12 | public override SyntaxKind Kind => SyntaxKind.ConstantNode;
13 | public override SyntaxCategory Category => SyntaxCategory.Value;
14 |
15 | public ConstantNode(SourceSpan span, string value, ConstantKind kind)
16 | : base(span, value)
17 | {
18 | ConstantKind = kind;
19 | }
20 | }
21 |
22 | public enum ConstantKind
23 | {
24 | Invalid,
25 | Null,
26 | Integer,
27 | Float,
28 | String,
29 | Boolean,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/FluiParser/Language/TokenKind.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language
6 | {
7 | public enum TokenKind
8 | {
9 | EndOfFile,
10 | Error,
11 |
12 | #region Whitespace
13 |
14 | WhiteSpace,
15 | NewLine,
16 | Indentation,
17 |
18 | #endregion
19 |
20 | #region Comments
21 |
22 | LineComment,
23 | BlockComment,
24 |
25 | #endregion
26 |
27 | #region Constants
28 |
29 | IntegerLiteral,
30 | StringLiteral,
31 | FloatLiteral,
32 | BooleanLiteral,
33 |
34 | #endregion
35 |
36 | #region Identifiers
37 |
38 | Identifier,
39 | Widget,
40 |
41 | #endregion
42 |
43 | #region Punctuation
44 |
45 | Dot,
46 | Colon,
47 | Comma,
48 | DollarSign,
49 | Ampersand,
50 |
51 | #endregion
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/FluiParser/Utility/ExtensionMethods.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Utility
6 | {
7 | public static class ExtensionMethods
8 | {
9 | public static char CharAt(this string str, int index)
10 | {
11 | if (index >= str.Length || index < 0)
12 | {
13 | return '\0';
14 | }
15 |
16 | return str[index];
17 | }
18 |
19 | public static string PascalCaseToUnderscore(this string str)
20 | {
21 | StringBuilder builder = new StringBuilder();
22 |
23 | char c;
24 | for (int i = 0; i < str.Length; i++)
25 | {
26 | c = str[i];
27 | if (Char.IsUpper(c) && builder.Length > 0)
28 | {
29 | builder.Append('_');
30 | }
31 |
32 | builder.Append(Char.ToLower(c));
33 | }
34 |
35 | return builder.ToString();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/FluiParser/schema.ebnf:
--------------------------------------------------------------------------------
1 | document = widget_type, white_space, view_model_attr, white_space, identifier, desc_marker, new_line, descendent;
2 | descendent = indentation, ( node | attribute | value );
3 | node = identifier, [ white_space, attribute ], [{ attr_separator, white_space, attribute }], [ desc_marker, { new_line, descendent }];
4 | attribute = attr_marker, identifier, white_space, ( node | value | desc_marker, new_line, descendent );
5 | value = ( bool_literal | num_literal | string_literal );
6 |
7 | line_comment = /!.*?$/;
8 | block_comment = /!!.*?!!/;
9 |
10 | bool_literal = ( "true" | "false" );
11 | num_literal = ( int_literal | hex_literal | float_literal | exp_literal );
12 | string_literal = /(".*?(? (ViewClass.Child as NodeWithValue).Value;
16 | [Newtonsoft.Json.JsonIgnore()]
17 | public String ViewModelClassName => (ViewModelClass.Child as NodeWithValue).Value;
18 |
19 | [Newtonsoft.Json.JsonIgnore()]
20 | public SourceCode SourceCode { get; }
21 |
22 | public SourceDocument(SourceCode sourceCode, WidgetKind kind, AttributeSingleNode viewModelClass, AttributeSingleNode viewClass)
23 | {
24 | SourceCode = sourceCode;
25 | Kind = kind;
26 | ViewModelClass = viewModelClass;
27 | ViewClass = viewClass;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/FluiParser.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27428.2005
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluiParser", "FluiParser\FluiParser.csproj", "{DB1379FB-AC5B-4256-BC9A-CA38FF38A1BB}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {DB1379FB-AC5B-4256-BC9A-CA38FF38A1BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {DB1379FB-AC5B-4256-BC9A-CA38FF38A1BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {DB1379FB-AC5B-4256-BC9A-CA38FF38A1BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {DB1379FB-AC5B-4256-BC9A-CA38FF38A1BB}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {457869E2-BB1C-4945-9D05-FC27352EEE0F}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/FluiParser/Language/SourceLocation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language
6 | {
7 | public struct SourceLocation : IEquatable
8 | {
9 | private readonly int _column;
10 | private readonly int _index;
11 | private readonly int _line;
12 |
13 | public int Column => _column;
14 | public int Index => _index;
15 | public int Line => _line;
16 |
17 | public SourceLocation(int index, int line, int column)
18 | {
19 | _index = index;
20 | _line = line;
21 | _column = column;
22 | }
23 |
24 | public static bool operator !=(SourceLocation left, SourceLocation right)
25 | {
26 | return !left.Equals(right);
27 | }
28 |
29 | public static bool operator ==(SourceLocation left, SourceLocation right)
30 | {
31 | return left.Equals(right);
32 | }
33 |
34 | public override bool Equals(object obj)
35 | {
36 | if (obj is SourceLocation)
37 | {
38 | return Equals((SourceLocation)obj);
39 | }
40 | return base.Equals(obj);
41 | }
42 |
43 | public bool Equals(SourceLocation other)
44 | {
45 | return other.GetHashCode() == GetHashCode();
46 | }
47 |
48 | public override int GetHashCode()
49 | {
50 | return 0xB1679EE ^ Index ^ Line ^ Column;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/FluiParser/Language/SourceSpan.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language
6 | {
7 | public struct SourceSpan
8 | {
9 | private readonly SourceLocation _start;
10 | private readonly SourceLocation _end;
11 |
12 | public SourceLocation Start => _start;
13 | public SourceLocation End => _end;
14 | public int Length => _end.Index - _start.Index;
15 |
16 | public SourceSpan(SourceLocation start, SourceLocation end)
17 | {
18 | _start = start;
19 | _end = end;
20 | }
21 |
22 | public static bool operator !=(SourceSpan left, SourceSpan right)
23 | {
24 | return !left.Equals(right);
25 | }
26 |
27 | public static bool operator ==(SourceSpan left, SourceSpan right)
28 | {
29 | return left.Equals(right);
30 | }
31 |
32 | public override bool Equals(object obj)
33 | {
34 | if (obj is SourceSpan)
35 | {
36 | return Equals((SourceSpan)obj);
37 | }
38 | return base.Equals(obj);
39 | }
40 |
41 | public bool Equals(SourceSpan other)
42 | {
43 | return GetHashCode() == other.GetHashCode();
44 | }
45 |
46 | public override int GetHashCode()
47 | {
48 | return 0x509CE ^ Start.GetHashCode() ^ End.GetHashCode();
49 | }
50 |
51 | public override string ToString()
52 | {
53 | return $"{_start.Line} {_start.Column} {Length}";
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/FluiParser/Program.cs:
--------------------------------------------------------------------------------
1 | using FluiParser.Language;
2 | using FluiParser.Language.Generator;
3 | using FluiParser.Language.Parser;
4 | using FluiParser.Language.Tokenizer;
5 | using FluiParser.Utility;
6 | using System;
7 | using System.IO;
8 | using System.Linq;
9 |
10 | namespace FluiParser
11 | {
12 | class Program
13 | {
14 | static void Main(string[] args)
15 | {
16 | string testFile = "sample.flui";
17 | string fileprefix = Path.GetFileNameWithoutExtension(testFile);
18 | SourceCode code = new SourceCode(File.ReadAllText(testFile));
19 |
20 | var tokens = Tokenizer.Instance.TokenizeFile(code).ToList();
21 | var errors = Tokenizer.Instance.ErrorSink.ToList();
22 |
23 | File.WriteAllText("tokens.json", Newtonsoft.Json.JsonConvert.SerializeObject(tokens, Newtonsoft.Json.Formatting.Indented));
24 | File.WriteAllText("token_errors.json", Newtonsoft.Json.JsonConvert.SerializeObject(errors, Newtonsoft.Json.Formatting.Indented));
25 |
26 | var symbolDoc = Parser.Instance.ParseFile(code, tokens);
27 |
28 | File.WriteAllText("symbols.json", Newtonsoft.Json.JsonConvert.SerializeObject(symbolDoc, Newtonsoft.Json.Formatting.Indented));
29 |
30 | var view = Generator.Instance.GenerateViewFile(symbolDoc);
31 | var viewModel = Generator.Instance.GenerateViewModelFile(symbolDoc);
32 |
33 | File.WriteAllText($"{symbolDoc.ViewClassName.PascalCaseToUnderscore()}.dart", view);
34 | File.WriteAllText($"{symbolDoc.ViewModelClassName.PascalCaseToUnderscore()}.dart", viewModel);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/FluiParser/Language/Generator/Generator.cs:
--------------------------------------------------------------------------------
1 | using FluiParser.Language.Syntax;
2 | using FluiParser.Language.Syntax.Nodes;
3 | using FluiParser.Utility;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text;
7 |
8 | namespace FluiParser.Language.Generator
9 | {
10 | public sealed partial class Generator
11 | {
12 | private static Lazy _inst = new Lazy();
13 | public static Generator Instance => _inst.Value;
14 |
15 | private StringBuilder _builder = new StringBuilder();
16 | private GeneratorOptions _options;
17 | private SourceDocument _sourceDoc;
18 |
19 | private void Initialize(SourceDocument sourceDocument, GeneratorOptions options)
20 | {
21 | _sourceDoc = sourceDocument;
22 | _options = options;
23 | _builder.Clear();
24 | }
25 |
26 | public string GenerateViewFile(SourceDocument sourceDocument) => GenerateViewFile(sourceDocument, GeneratorOptions.Default);
27 | public string GenerateViewFile(SourceDocument sourceDocument, GeneratorOptions options)
28 | {
29 | Initialize(sourceDocument, options);
30 |
31 | ParseSymbolsForView();
32 |
33 | return _builder.ToString();
34 | }
35 |
36 | public string GenerateViewModelFile(SourceDocument sourceDocument) => GenerateViewModelFile(sourceDocument, GeneratorOptions.Default);
37 | public string GenerateViewModelFile(SourceDocument sourceDocument, GeneratorOptions options)
38 | {
39 | Initialize(sourceDocument, options);
40 |
41 | ParseSymbolsForViewModel();
42 |
43 | return _builder.ToString();
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/FluiParser/Language/ErrorSink.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace FluiParser.Language
7 | {
8 | public sealed class ErrorEntry
9 | {
10 | public string[] Lines { get; }
11 | public string Message { get; }
12 | public Severity Severity { get; }
13 | public SourceSpan Span { get; }
14 | public ErrorEntry(string msg, string[] lines, Severity severity, SourceSpan span)
15 | {
16 | Message = msg;
17 | Lines = lines;
18 | Span = span;
19 | Severity = severity;
20 | }
21 | }
22 |
23 | public sealed class ErrorSink : IEnumerable
24 | {
25 | private List _errors;
26 |
27 | public IEnumerable Errors => _errors.AsReadOnly();
28 | public bool HasErrors => _errors.Count > 0;
29 |
30 | public ErrorSink()
31 | {
32 | _errors = new List();
33 | }
34 |
35 | public void AddError(string msg, SourceCode code, Severity severity, SourceSpan span)
36 | {
37 | _errors.Add(new ErrorEntry(msg, code.GetLines(span.Start.Line, span.End.Line), severity, span));
38 | }
39 |
40 | public void Clear()
41 | {
42 | _errors.Clear();
43 | }
44 |
45 | public IEnumerator GetEnumerator()
46 | {
47 | return _errors.GetEnumerator();
48 | }
49 |
50 | IEnumerator IEnumerable.GetEnumerator()
51 | {
52 | return _errors.GetEnumerator();
53 | }
54 | }
55 |
56 | public enum Severity
57 | {
58 | None,
59 | Message,
60 | Warning,
61 | Error,
62 | Fatal
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/FluiParser/Language/Generator/Generator.ViewModel.cs:
--------------------------------------------------------------------------------
1 | using FluiParser.Language.Syntax;
2 | using FluiParser.Language.Syntax.Nodes;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace FluiParser.Language.Generator
8 | {
9 | public sealed partial class Generator
10 | {
11 | private static readonly string _viewModelHeader = "import 'package:flutter/material.dart';\n\nclass {0} extends StatelessWidget {{\n";
12 | private static readonly string _viewModelFooter = "\n}\n";
13 |
14 | private static readonly string _identifierViewModel = "{0}var {1} = null; // TODO: Populate field {1}";
15 | private static readonly string _functionViewModel = "{0}{1}() {{\n{0}{0}// TODO: Populate function {1}\n{0}}}";
16 |
17 | private bool _memberAdded;
18 |
19 | private string ViewModelIndent => new string(_options.IndentationCharacter, _options.IndentationLength);
20 |
21 | private void ParseSymbolsForViewModel()
22 | {
23 | _memberAdded = false;
24 |
25 | string viewModelClass = _sourceDoc.ViewModelClassName;
26 | _builder.Append(string.Format(_viewModelHeader, viewModelClass));
27 |
28 | ElementSingleNode root = _sourceDoc.ViewClass.Child as ElementSingleNode;
29 | ParseNode(root);
30 |
31 | _builder.Append(_viewModelFooter);
32 | }
33 |
34 | private void ParseNode(SyntaxNode node)
35 | {
36 | switch (node.Kind)
37 | {
38 | case SyntaxKind.ElementNode:
39 | foreach (var child in (node as ElementNode).Children) ParseNode(child);
40 | break;
41 |
42 | case SyntaxKind.AttributeNode:
43 | foreach (var child in (node as AttributeNode).Children) ParseNode(child);
44 | break;
45 |
46 | case SyntaxKind.ElementSingleNode:
47 | ParseNode((node as ElementSingleNode).Child);
48 | break;
49 |
50 | case SyntaxKind.AttributeSingleNode:
51 | ParseNode((node as AttributeSingleNode).Child);
52 | break;
53 |
54 | case SyntaxKind.IdentifierNode:
55 | ParseIdentifierViewModel(node as IdentifierNode);
56 | break;
57 |
58 | case SyntaxKind.FunctionCallNode:
59 | ParseFunctionCallViewModel(node as FunctionCallNode);
60 | break;
61 |
62 | case SyntaxKind.CallbackNode:
63 | ParseCallbackViewModel(node as CallbackNode);
64 | break;
65 |
66 | case SyntaxKind.ConstantNode:
67 | break;
68 |
69 | default:
70 | throw new NotImplementedException($"Unrecognized node type {node.Kind}");
71 | }
72 | }
73 |
74 | private void ParseFunctionCallViewModel(FunctionCallNode node)
75 | {
76 | if (_memberAdded)
77 | {
78 | _builder.AppendLine();
79 | _builder.AppendLine();
80 | }
81 |
82 | _builder.Append(string.Format(_functionViewModel, ViewModelIndent, node.Value));
83 |
84 | _memberAdded = true;
85 | }
86 |
87 | private void ParseCallbackViewModel(CallbackNode node)
88 | {
89 | if (_memberAdded)
90 | {
91 | _builder.AppendLine();
92 | _builder.AppendLine();
93 | }
94 |
95 | _builder.Append(string.Format(_functionViewModel, ViewModelIndent, node.Value));
96 |
97 | _memberAdded = true;
98 | }
99 |
100 | private void ParseIdentifierViewModel(IdentifierNode node)
101 | {
102 | if (_memberAdded)
103 | {
104 | _builder.AppendLine();
105 | _builder.AppendLine();
106 | }
107 |
108 | _builder.Append(string.Format(_identifierViewModel, ViewModelIndent, node.Value));
109 |
110 | _memberAdded = true;
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/FluiParser/Language/Token.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FluiParser.Language
6 | {
7 | public class Token
8 | {
9 | private Lazy _catagory;
10 |
11 | [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
12 | public TokenCategory Catagory => _catagory.Value;
13 | [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
14 | public TokenKind Kind { get; }
15 | public SourceSpan Span { get; }
16 | public string Value { get; }
17 |
18 | public bool IsTrivia => Kind == TokenKind.WhiteSpace || Catagory == TokenCategory.Comment;
19 |
20 | public Token(TokenKind kind, string value, SourceLocation start, SourceLocation end)
21 | {
22 | Kind = kind;
23 | Value = value;
24 | Span = new SourceSpan(start, end);
25 |
26 | _catagory = new Lazy(GetTokenCategory);
27 | }
28 |
29 | public static bool operator !=(Token left, string right) => left?.Value != right;
30 | public static bool operator !=(string left, Token right) => left != right?.Value;
31 | public static bool operator !=(Token left, TokenKind right) => left?.Kind != right;
32 | public static bool operator !=(TokenKind left, Token right) => left != right?.Kind;
33 | public static bool operator !=(Token left, TokenCategory right) => left?.Catagory != right;
34 | public static bool operator !=(TokenCategory left, Token right) => left != right?.Catagory;
35 |
36 | public static bool operator ==(Token left, string right) => left?.Value == right;
37 | public static bool operator ==(string left, Token right) => left == right?.Value;
38 | public static bool operator ==(Token left, TokenKind right) => left?.Kind == right;
39 | public static bool operator ==(TokenKind left, Token right) => left == right?.Kind;
40 | public static bool operator ==(Token left, TokenCategory right) => left?.Catagory == right;
41 | public static bool operator ==(TokenCategory left, Token right) => left == right?.Catagory;
42 |
43 | public override bool Equals(object obj)
44 | {
45 | if (obj is Token)
46 | {
47 | return Equals((Token)obj);
48 | }
49 | return base.Equals(obj);
50 | }
51 |
52 | public bool Equals(Token other)
53 | {
54 | if (other == null) return false;
55 | return other.Value == Value &&
56 | other.Span == Span &&
57 | other.Kind == Kind;
58 | }
59 |
60 | public override int GetHashCode()
61 | {
62 | return Value.GetHashCode() ^ Span.GetHashCode() ^ Kind.GetHashCode();
63 | }
64 |
65 | public override string ToString()
66 | {
67 | return $"Kind: {Kind} - Value: {Value}";
68 | }
69 |
70 | private TokenCategory GetTokenCategory()
71 | {
72 | switch (Kind)
73 | {
74 | case TokenKind.Colon:
75 | case TokenKind.Dot:
76 | case TokenKind.Comma:
77 | case TokenKind.DollarSign:
78 | case TokenKind.Ampersand:
79 | return TokenCategory.Punctuation;
80 |
81 | case TokenKind.BlockComment:
82 | case TokenKind.LineComment:
83 | return TokenCategory.Comment;
84 |
85 | case TokenKind.NewLine:
86 | case TokenKind.WhiteSpace:
87 | case TokenKind.Indentation:
88 | return TokenCategory.WhiteSpace;
89 |
90 | case TokenKind.Identifier:
91 | case TokenKind.Widget:
92 | return TokenCategory.Identifier;
93 |
94 | case TokenKind.StringLiteral:
95 | case TokenKind.IntegerLiteral:
96 | case TokenKind.FloatLiteral:
97 | case TokenKind.BooleanLiteral:
98 | return TokenCategory.Constant;
99 |
100 | case TokenKind.EndOfFile:
101 | return TokenCategory.Metadata;
102 |
103 | case TokenKind.Error:
104 | return TokenCategory.Invalid;
105 |
106 | default:
107 | return TokenCategory.Unknown;
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/FluiParser/Language/SourceCode.cs:
--------------------------------------------------------------------------------
1 | using FluiParser.Language.Syntax;
2 | using FluiParser.Utility;
3 | using System;
4 | using System.Collections;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 |
9 | namespace FluiParser.Language
10 | {
11 | public sealed class SourceCode
12 | {
13 | private Lazy _lines;
14 | private string _sourceCode;
15 |
16 | public string Contents => _sourceCode;
17 | public string[] Lines => _lines.Value;
18 |
19 | public char this[int index]
20 | {
21 | get { return _sourceCode.CharAt(index); }
22 | }
23 |
24 | private class Subset : IEnumerable
25 | {
26 | private int _start;
27 | private int _end;
28 | private IEnumerable _set;
29 |
30 | private struct SubsetEnumerator : IEnumerator
31 | {
32 | private bool _disposed;
33 | private int _index;
34 | private Subset _subset;
35 |
36 | public T Current => _subset._set.ElementAt(_subset._start + _index);
37 |
38 | object IEnumerator.Current => _subset._set.ElementAt(_subset._start + _index);
39 |
40 | public SubsetEnumerator(Subset subset)
41 | {
42 | _disposed = false;
43 | _index = subset._start - 1;
44 | _subset = subset;
45 | }
46 |
47 | public void Dispose()
48 | {
49 | _disposed = true;
50 | }
51 |
52 | public bool MoveNext()
53 | {
54 | if (_disposed)
55 | {
56 | throw new ObjectDisposedException("SubsetEnumerator");
57 | }
58 |
59 | if (_index == _subset._end)
60 | {
61 | return false;
62 | }
63 |
64 | _index++;
65 |
66 | return true;
67 | }
68 |
69 | public void Reset()
70 | {
71 | if (_disposed)
72 | {
73 | throw new ObjectDisposedException("SubsetEnumerator");
74 | }
75 |
76 | _index = _subset._start;
77 | }
78 | }
79 |
80 | public Subset(IEnumerable collection, int start, int end)
81 | {
82 | _set = collection;
83 | _start = start;
84 | _end = end;
85 | }
86 |
87 | public IEnumerator GetEnumerator()
88 | {
89 | return new SubsetEnumerator(this);
90 | }
91 |
92 | IEnumerator IEnumerable.GetEnumerator()
93 | {
94 | return new SubsetEnumerator(this);
95 | }
96 | }
97 |
98 | public SourceCode(string sourceCode)
99 | {
100 | _sourceCode = sourceCode;
101 | _lines = new Lazy(() => _sourceCode.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
102 | }
103 |
104 | public string GetLine(int line)
105 | {
106 | if (line < 1)
107 | {
108 | throw new IndexOutOfRangeException($"{nameof(line)} must not be less than 1!");
109 | }
110 | if (line > Lines.Length)
111 | {
112 | throw new IndexOutOfRangeException($"No line {line}!");
113 | }
114 |
115 | return Lines[line - 1];
116 | }
117 |
118 | public string[] GetLines(int start, int end)
119 | {
120 | if (end < start)
121 | {
122 | throw new IndexOutOfRangeException("Cannot retrieve negative range!");
123 | }
124 | if (start < 1)
125 | {
126 | throw new IndexOutOfRangeException($"{nameof(start)} must not be less than 1!");
127 | }
128 | if (end > Lines.Length)
129 | {
130 | throw new IndexOutOfRangeException("Cannot retrieve more lines than exist in file!");
131 | }
132 |
133 | return new Subset(Lines, start - 1, end - 1).ToArray();
134 | }
135 |
136 | public string GetSpan(SourceSpan span)
137 | {
138 | int start = span.Start.Index;
139 | int length = span.Length;
140 | return _sourceCode.Substring(start, length);
141 | }
142 |
143 | public string GetSpan(SyntaxNode node)
144 | {
145 | return GetSpan(node.Span);
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/FluiParser/Language/Generator/Generator.View.cs:
--------------------------------------------------------------------------------
1 | using FluiParser.Language.Syntax;
2 | using FluiParser.Language.Syntax.Nodes;
3 | using FluiParser.Utility;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text;
7 |
8 | namespace FluiParser.Language.Generator
9 | {
10 | public sealed partial class Generator
11 | {
12 | private static readonly string _viewHeader = "// DO NOT WRITE CODE IN THIS FILE\n// IT WILL GET OVERWRITTEN WHEN THE UI CODE IS REBUILT\n\nimport 'package:flutter/material.dart';\n\nimport './{0}.dart';\n\nclass {1} extends {2} {{\n{3}@override\n{3}Widget build(BuildContext context) {{\n{3}{3}return ";
13 | private static readonly string _viewFooter = ";\n{0}}}\n}}";
14 |
15 | private int _indentLevel;
16 | private string ViewIndent => new string(_options.IndentationCharacter, _indentLevel * _options.IndentationLength);
17 |
18 | private void ParseSymbolsForView()
19 | {
20 | _indentLevel = 2;
21 |
22 | string viewModelClass = _sourceDoc.ViewModelClassName;
23 | string viewClass = _sourceDoc.ViewClassName;
24 | _builder.Append(string.Format(_viewHeader, viewModelClass.PascalCaseToUnderscore(), viewClass, viewModelClass, ViewModelIndent));
25 |
26 | ElementSingleNode container = _sourceDoc.ViewClass.Child as ElementSingleNode;
27 | SyntaxNode root = container.Child;
28 | ParseSymbol(root, false, true);
29 |
30 | _builder.Append(string.Format(_viewFooter, ViewModelIndent));
31 | }
32 |
33 | private void ParseSymbol(SyntaxNode node, bool insertIndent = true, bool isRoot = false)
34 | {
35 | if (insertIndent)
36 | {
37 | _builder.Append(ViewIndent);
38 | }
39 |
40 | switch (node.Kind)
41 | {
42 | case SyntaxKind.ElementNode:
43 | ParseElement(node as ElementNode);
44 | break;
45 |
46 | case SyntaxKind.ElementSingleNode:
47 | ParseElementSingle(node as ElementSingleNode);
48 | break;
49 |
50 | case SyntaxKind.AttributeNode:
51 | ParseAttribute(node as AttributeNode);
52 | break;
53 |
54 | case SyntaxKind.AttributeSingleNode:
55 | ParseAttributeSingle(node as AttributeSingleNode);
56 | break;
57 |
58 | case SyntaxKind.IdentifierNode:
59 | ParseIdentifier(node as IdentifierNode);
60 | break;
61 |
62 | case SyntaxKind.FunctionCallNode:
63 | ParseFunctionCall(node as FunctionCallNode);
64 | break;
65 |
66 | case SyntaxKind.CallbackNode:
67 | ParseCallback(node as CallbackNode);
68 | break;
69 |
70 | case SyntaxKind.ConstantNode:
71 | ParseConstant(node as ConstantNode);
72 | break;
73 |
74 | default:
75 | throw new NotImplementedException($"Unrecognized node type {node.Kind}");
76 | }
77 |
78 | if (isRoot)
79 | {
80 | if (_builder.ToString().EndsWith(','))
81 | {
82 | _builder.Remove(_builder.Length - 1, 1);
83 | }
84 | }
85 | }
86 |
87 | private void ParseElement(ElementNode node)
88 | {
89 | _builder.Append($"{node.Value} (");
90 |
91 | _indentLevel++;
92 |
93 | foreach (var child in node.Children)
94 | {
95 | _builder.AppendLine();
96 | ParseSymbol(child);
97 |
98 | if (child.Category == SyntaxCategory.Value || child.Kind == SyntaxKind.AttributeSingleNode || child.Kind == SyntaxKind.ElementSingleNode)
99 | {
100 | _builder.Append(",");
101 | }
102 | }
103 |
104 | _indentLevel--;
105 |
106 | _builder.AppendLine();
107 | _builder.Append($"{ViewIndent}),");
108 | }
109 |
110 | private void ParseElementSingle(ElementSingleNode node)
111 | {
112 | _builder.Append($"{node.Value}(");
113 |
114 | if (node.Child.Category == SyntaxCategory.Value)
115 | {
116 | ParseSymbol(node.Child, false);
117 | _builder.Append($"),");
118 | }
119 | else
120 | {
121 | _builder.AppendLine();
122 | _indentLevel++;
123 | ParseSymbol(node.Child);
124 | _indentLevel--;
125 | _builder.AppendLine();
126 | _builder.Append($"{ViewIndent}),");
127 | }
128 | }
129 |
130 | private void ParseAttribute(AttributeNode node)
131 | {
132 | _builder.Append($"{node.Value}: [");
133 |
134 | _indentLevel++;
135 |
136 | foreach (var child in node.Children)
137 | {
138 | _builder.AppendLine();
139 | ParseSymbol(child);
140 | }
141 |
142 | _indentLevel--;
143 |
144 | _builder.AppendLine();
145 | _builder.Append($"{ViewIndent}],");
146 | }
147 |
148 | private void ParseAttributeSingle(AttributeSingleNode node)
149 | {
150 | _builder.Append($"{node.Value}: ");
151 | ParseSymbol(node.Child, false);
152 | }
153 |
154 | private void ParseIdentifier(IdentifierNode node)
155 | {
156 | _builder.Append(node.Value);
157 | }
158 |
159 | private void ParseCallback(CallbackNode node)
160 | {
161 | _builder.Append($"() => {node.Value}()");
162 | }
163 |
164 | private void ParseFunctionCall(FunctionCallNode node)
165 | {
166 | _builder.Append($"{node.Value}()");
167 | }
168 |
169 | private void ParseConstant(ConstantNode node)
170 | {
171 | switch (node.ConstantKind)
172 | {
173 | case ConstantKind.Null:
174 | _builder.Append("null");
175 | break;
176 |
177 | case ConstantKind.Boolean:
178 | case ConstantKind.Integer:
179 | case ConstantKind.Float:
180 | _builder.Append(node.Value);
181 | break;
182 |
183 | case ConstantKind.String:
184 | _builder.Append($"'{node.Value}'");
185 | break;
186 |
187 | default:
188 | throw new NotImplementedException($"Unrecognized constnat type {node.ConstantKind}");
189 | }
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | .vscode/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # Visual Studio 2017 auto generated files
34 | Generated\ Files/
35 |
36 | # MSTest test Results
37 | [Tt]est[Rr]esult*/
38 | [Bb]uild[Ll]og.*
39 |
40 | # NUNIT
41 | *.VisualState.xml
42 | TestResult.xml
43 |
44 | # Build Results of an ATL Project
45 | [Dd]ebugPS/
46 | [Rr]eleasePS/
47 | dlldata.c
48 |
49 | # Benchmark Results
50 | BenchmarkDotNet.Artifacts/
51 |
52 | # .NET Core
53 | project.lock.json
54 | project.fragment.lock.json
55 | artifacts/
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FLUI - A UI file format for Flutter
2 |
3 | FLUI is an entirely new data storage format created especially for use in Flutter apps. It is loosely based on the YAML syntax but designed to resemble the resulting Dart code in organization. Using this format (and the bundled builder) you will be able to write your UI code in a centralized place. Once converted, the build script converts the UI code into separate View and ViewModel classes.
4 |
5 | Conceptually, the approach for View-ViewModel separation comes from [this article by Edrick Leong](https://blog.usejournal.com/easily-navigate-through-your-flutter-code-by-separating-view-and-view-model-240026191106). This is about the most abstracted way I could find to separate the build function from the other code in Widget classes. Every other method I can think of would require the use of the `dart:mirror` package (i.e. Reflection), which Flutter [doesn't currently support for optimization reasons](https://github.com/flutter/flutter/issues/1150).
6 |
7 | ## Examples
8 |
9 | Here is an example `sample.flui` file:
10 |
11 | ```
12 | Stateless .viewModel SampleViewModel, .view SampleView:
13 | Center .child:
14 | Column .children:
15 | ! Child 1
16 | Card .child:
17 | Column .children:
18 | Text 'Hello World'
19 | Text 'Flui says hello'
20 | ! Child 2
21 | Card .child:
22 | Column .children:
23 | Text 'Hello again, World', .style textStyle
24 | Text:
25 | 'Flui wants a bagel'
26 | .style $getStyle
27 | RaisedButton:
28 | .title 'Get Flui a bagel'
29 | .onPressed @getBagel
30 | ```
31 |
32 | After being run through the build script, This code gets converted into the following:
33 |
34 | `sample_view_model.dart`
35 | ```dart
36 | import 'package:flutter/material.dart';
37 |
38 | import './sample_view_model.dart';
39 |
40 | class SampleView extends SampleViewModel {
41 | @override
42 | Widget build(BuildContext context) {
43 | return Center(
44 | child: Column(
45 | children: [
46 | Card(
47 | child: Column(
48 | children: [
49 | Text('Hello World'),
50 | Text('Flui says hello'),
51 | ],
52 | ),
53 | ),
54 | Card(
55 | child: Column(
56 | children: [
57 | Text (
58 | 'Hello again, World',
59 | style: textStyle,
60 | ),
61 | Text (
62 | 'Flui wants a bagel',
63 | style: getStyle(),
64 | ),
65 | RaisedButton (
66 | title: 'Give Flui a bagel',
67 | onPressed: () => giveBagel(),
68 | ),
69 | ],
70 | ),
71 | ),
72 | ],
73 | ),
74 | );
75 | }
76 | }
77 | ```
78 |
79 | `sample_view.dart`
80 | ```dart
81 | import 'package:flutter/material.dart';
82 |
83 | class SampleViewModel extends StatelessWidget {
84 | var textStyle = null; // TODO: Populate field textStyle
85 |
86 | getStyle() {
87 | // TODO: Populate function getStyle
88 | }
89 |
90 | giveBagel() {
91 | // TODO: Populate function giveBagel
92 | }
93 | }
94 | ```
95 |
96 | The view file is entirely managed by the build script - any code written into the file would be overwritten the next time the buildscript is run.
97 |
98 | ## Syntax
99 |
100 | As far as syntax goes, the file is incredibly simple.
101 |
102 | ```
103 | Stateless .viewModel SampleViewModel, .view SampleView:
104 | ```
105 |
106 | This is the file header and the root node for the entire document. It requires either `Stateless` or `Stateful` as the first value. It also requires the two attributes `.view` and `.viewModel`, the values of which are used to determine the class references and names of the generated `.dart` files and can be anything that is a valid Dart class name.
107 |
108 | From there, the UI hierarchy is determined by indentation level.
109 |
110 | ```
111 | Center .child:
112 | ```
113 |
114 | The first value is the name of an identifier for a UI element. (While it is not currently mandatory that the value be an _actual_ Flutter widget, the build script does differentiate between Flutter Widget names and other ordinary identifiers. This might become significant in the future for the building process, tooling for VS Code, etc.)
115 |
116 | After the first identifier, you can list the attributes of the widget by using the period character `.` followed by the identifier of the attribute and then the value of the attribute.
117 |
118 | For widgets that support a positional argument, you can specify a value directly without attaching it to an attribute, such as with `Text` widget:
119 |
120 | ```
121 | Text 'Hello World'
122 | ```
123 |
124 | ### Single Line vs Multiline
125 |
126 | Widgets and attributes can be supplied with values either on the same line as the identifier or on the next line with an increased indentation level. For example:
127 |
128 | ```
129 | Text 'Hello World'
130 | ```
131 |
132 | is functionally identical to:
133 |
134 | ```
135 | Text:
136 | 'Hello World'
137 | ```
138 |
139 | For single line widget declarations, you separate multiple attributes using a comma:
140 |
141 | ```
142 | Text 'Hello again, World', .style textStyle
143 | ```
144 |
145 | As a general rule, you use a colon `:` to designate when a widget or attribute is listing its child(ren) on the next line. The colon is optional, however, so you could just as easily do:
146 |
147 | ```
148 | Text
149 | 'Hello World'
150 | ```
151 |
152 | ### ViewModel References
153 |
154 | There are three different ways to directly reference an object that exists in the view model. First, you can reference a field object by typing the name of the object:
155 |
156 | ```
157 | Text 'Hello again, World', .style textStyle
158 | ```
159 |
160 | This ties directly to this line in the view model:
161 |
162 | ```
163 | var textStyle = null; // TODO: Populate field textStyle
164 | ```
165 |
166 | Next, you can call a function that returns a value by using the dollar sign `$` prefix:
167 |
168 | ```
169 | Text:
170 | 'Flui wants a bagel'
171 | .style $getStyle
172 | ```
173 |
174 | This calls this function in the view model:
175 |
176 | ```
177 | getStyle() {
178 | // TODO: Populate function getStyle
179 | }
180 | ```
181 |
182 | Finally, you can specify a callback for an action using the prefix `@`:
183 |
184 | ```
185 | RaisedButton:
186 | .title 'Give Flui a bagel'
187 | .onPressed @giveBagel
188 | ```
189 |
190 | In the view, this gets translated to the following:
191 |
192 | ```
193 | RaisedButton (
194 | title: 'Give Flui a bagel',
195 | onPressed: () => giveBagel(),
196 | ),
197 | ```
198 |
199 | Which, obviously, calls this function in the view model:
200 |
201 | ```
202 | giveBagel() {
203 | // TODO: Populate function giveBagel
204 | }
205 | ```
206 |
207 | ### Commenting
208 |
209 | FLUI supports commenting with both line comments and block comments. Line comments are done by using the exclamation mark `!` character:
210 |
211 | ```
212 | ! This is a line comment
213 | ```
214 |
215 | Block comments are done with a double explamation mark `!!` marking the beginning and end of the block:
216 |
217 | ```
218 | !! This
219 | is a
220 | block
221 | comment !!
222 | ```
223 |
224 | ## TODO
225 |
226 | This list in itself is a TODO, as I will probably think of new features and functions to add as the project comes along.
227 |
228 | - [ ] Add Stateful widget support
229 | - [ ] Intelligently add fields and functions to the view model so as to not overwrite existing changes
230 | - [ ] Eliminate common attributes from having to be explicitly stated (e.g. `.child` for `Center`, `.children` for `Column`, etc.)
231 | - [ ] VS Code plugin for syntax and colorization support
232 | - [ ] VS Code integration for detecting changes in UI files to update the view and view model files (which would, in tirn, trigger hot reloading for Flutter itself)
233 | - [ ] Hook up automated integration testing for future build changes
--------------------------------------------------------------------------------
/FluiParser/Language/Parser/Parser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Linq;
5 | using FluiParser.Language.Syntax;
6 | using FluiParser.Language.Syntax.Nodes;
7 |
8 | namespace FluiParser.Language.Parser
9 | {
10 | public sealed class Parser
11 | {
12 | private static Lazy _inst = new Lazy();
13 | public static Parser Instance => _inst.Value;
14 |
15 | private static readonly string[] rootNodes = { "Stateless", "Stateful" };
16 |
17 | private bool _error;
18 | private ErrorSink _errorSink;
19 | private int _index;
20 | private SourceCode _sourceCode;
21 | private ParserOptions _options;
22 | private IEnumerable _tokens;
23 |
24 | private Token _current => _tokens.ElementAtOrDefault(_index) ?? _tokens.Last();
25 | private Token _last => Peek(-1);
26 | private Token _next => Peek(1);
27 |
28 | public bool Error => _error;
29 |
30 | public Parser() : this(new ErrorSink()) { }
31 | public Parser(ErrorSink errorSink)
32 | {
33 | _errorSink = errorSink;
34 | }
35 |
36 | public SourceDocument ParseFile(SourceCode code, IEnumerable tokens) => ParseFile(code, tokens, ParserOptions.Default);
37 | public SourceDocument ParseFile(SourceCode code, IEnumerable tokens, ParserOptions options)
38 | {
39 | InitializeParser(code, tokens, options);
40 |
41 | try
42 | {
43 | return ParseDocument();
44 | }
45 | catch (SyntaxException ex)
46 | {
47 | Console.WriteLine(ex.StackTrace);
48 | return null;
49 | }
50 | }
51 |
52 | private void InitializeParser(SourceCode sourceCode, IEnumerable tokens, ParserOptions options)
53 | {
54 | _sourceCode = sourceCode;
55 | _tokens = tokens.Where(t => !t.IsTrivia);
56 | _tokens = _tokens.Where((t, i) => {
57 | if (t == TokenKind.Indentation)
58 | {
59 | Token last = _tokens.ElementAtOrDefault(i - 1);
60 | Token next = _tokens.ElementAtOrDefault(i + 1);
61 | if ((last == null || last == TokenKind.NewLine) && (next == null || next == TokenKind.NewLine))
62 | {
63 | return false;
64 | }
65 | }
66 | return true;
67 | }).ToArray();
68 | _options = ParserOptions.Default;
69 | _index = 0;
70 | }
71 |
72 | #region Navigation
73 | private Token Peek(int ahead) => _tokens.ElementAtOrDefault(_index + ahead) ?? _tokens.Last();
74 |
75 | private void Advance() => _index++;
76 | private void AdvanceNewLine()
77 | {
78 | while (_current == TokenKind.NewLine)
79 | {
80 | Take(TokenKind.NewLine);
81 | }
82 | }
83 |
84 | private Token Take()
85 | {
86 | var token = _current;
87 | Advance();
88 | return token;
89 | }
90 |
91 | private Token Take(TokenKind kind)
92 | {
93 | if (_current != kind) throw UnexpectedToken(kind);
94 | return Take();
95 | }
96 |
97 | private Token Take(params TokenKind[] kinds)
98 | {
99 | for (int i = 0; i < kinds.Length; i++)
100 | {
101 | if (_current == kinds[i])
102 | {
103 | return Take();
104 | }
105 | }
106 |
107 | throw UnexpectedToken(kinds);
108 | }
109 |
110 | private Token Take(string contextualKeyword)
111 | {
112 | if (_current != TokenKind.Identifier && _current != contextualKeyword) throw UnexpectedToken(contextualKeyword);
113 | return Take();
114 | }
115 |
116 | private Token TakeColon()
117 | {
118 | if (_options.EnforceColons || _current == TokenKind.Colon)
119 | {
120 | return Take(TokenKind.Colon);
121 | }
122 | return _current;
123 | }
124 | #endregion
125 |
126 | #region Node Parsing
127 | private SourceDocument ParseDocument()
128 | {
129 | WidgetKind kind;
130 |
131 | var token = Take(TokenKind.Identifier);
132 | if (!Enum.TryParse(token.Value, out kind))
133 | {
134 | throw SyntaxError(Severity.Fatal, "Root node identifier must indicate a widget type (e.g. Stateful, Stateless)");
135 | }
136 |
137 | var viewModelClass = ParseAttribute(0) as AttributeSingleNode;
138 | if (viewModelClass == null || viewModelClass.Value != "viewModel")
139 | {
140 | throw SyntaxError(Severity.Fatal, "Root node must have a specified `viewModel` attribute with a single identifier as a value");
141 | }
142 |
143 | Take(TokenKind.Comma);
144 |
145 | var viewClass = ParseAttribute(0) as AttributeSingleNode;
146 | if (viewClass == null || viewClass.Value != "view")
147 | {
148 | throw SyntaxError(Severity.Fatal, "Root node must have a specified `view` attribute with a single child widget as a value");
149 | }
150 |
151 | if (!(viewClass.Child is ElementSingleNode))
152 | {
153 | throw SyntaxError(Severity.Fatal, "The `view` attribute of the root ndoe must have a single widget as a child");
154 | }
155 |
156 | //if (_current != TokenKind.EndOfFile)
157 | //{
158 | // try
159 | // {
160 | // root = ParseNode() as ElementNode;
161 | // }
162 | // catch (SyntaxException) { }
163 | //}
164 |
165 | return new SourceDocument(_sourceCode, kind, viewModelClass, viewClass);
166 | }
167 |
168 | private SyntaxNode ParseNode(int indentLevel = 0)
169 | {
170 | SyntaxNode node;
171 |
172 | if (_current == TokenKind.Indentation)
173 | {
174 | var indent = _current;
175 | Advance();
176 | indentLevel = indent.Value.Length;
177 | }
178 |
179 | if (_current == TokenCategory.Constant)
180 | {
181 | node = ParseConstant();
182 | }
183 | else if (_current == TokenKind.Dot)
184 | {
185 | node = ParseAttribute(indentLevel);
186 | }
187 | else if (_current == TokenKind.DollarSign)
188 | {
189 | node = ParseFunctionCall();
190 | }
191 | else if (_current == TokenKind.Ampersand)
192 | {
193 | node = ParseCallback();
194 | }
195 | else
196 | {
197 | node = ParseElement(indentLevel);
198 | }
199 |
200 | return node;
201 | }
202 |
203 | private SyntaxNode ParseElement(int indentLevel)
204 | {
205 | Token start = Take(TokenKind.Identifier, TokenKind.Widget);
206 |
207 | #region Check if Identifier
208 | if (_current == TokenKind.Comma)
209 | {
210 | return new IdentifierNode(CreateSpan(start, start), start.Value);
211 | }
212 |
213 | if (_current != TokenKind.Dot && _current != TokenKind.Colon)
214 | {
215 | if (_current == TokenKind.EndOfFile)
216 | {
217 | return new IdentifierNode(CreateSpan(start, start), start.Value);
218 | }
219 |
220 | if (_current == TokenKind.NewLine)
221 | {
222 | Token next = Peek(1);
223 | if (next != TokenKind.Indentation || next.Span.Length <= indentLevel)
224 | {
225 | return new IdentifierNode(CreateSpan(start, start), start.Value);
226 | }
227 | }
228 | }
229 | #endregion
230 |
231 | List children = new List();
232 | SyntaxNode last = null;
233 | bool parsingChildNodes = true;
234 |
235 | do
236 | {
237 | while (_current != TokenKind.Colon && _current != TokenKind.Indentation && _current != TokenKind.NewLine && _current != TokenKind.EndOfFile)
238 | {
239 | last = ParseNode(indentLevel);
240 | children.Add(last);
241 | if (_current == TokenKind.Comma)
242 | {
243 | Take(TokenKind.Comma);
244 | }
245 | }
246 |
247 | if (_current == TokenKind.EndOfFile)
248 | {
249 | break;
250 | }
251 |
252 | if (_current == TokenKind.Comma)
253 | {
254 | Advance();
255 | break;
256 | }
257 |
258 | if (last == null || (last.Category != SyntaxCategory.Value))
259 | {
260 | TakeColon();
261 | }
262 |
263 | AdvanceNewLine();
264 |
265 | if (_current == TokenKind.Indentation && _current.Span.Length > indentLevel)
266 | {
267 | children.Add(ParseNode());
268 | }
269 | else
270 | {
271 | parsingChildNodes = false;
272 | }
273 | } while (parsingChildNodes);
274 |
275 | if (children.Count == 1)
276 | {
277 | return new ElementSingleNode(CreateSpan(start), start.Value, children[0]);
278 | }
279 |
280 | return new ElementNode(CreateSpan(start), start.Value, children.ToArray());
281 | }
282 |
283 | private NodeWithValue ParseAttribute(int indentLevel)
284 | {
285 | Token start = Take(TokenKind.Dot);
286 | Token attr = Take(TokenKind.Identifier);
287 |
288 | if (_current == TokenKind.Colon || _current == TokenKind.NewLine)
289 | {
290 | TakeColon();
291 | AdvanceNewLine();
292 |
293 | List children = new List();
294 | bool parsingChildNodes = true;
295 |
296 | do
297 | {
298 | if (_current == TokenKind.Indentation && _current.Span.Length > indentLevel)
299 | {
300 | children.Add(ParseNode());
301 | }
302 | else
303 | {
304 | parsingChildNodes = false;
305 | }
306 | } while (parsingChildNodes);
307 |
308 | if (children.Count == 1)
309 | {
310 | return new AttributeSingleNode(CreateSpan(start), attr.Value, children[0]);
311 | }
312 |
313 | return new AttributeNode(CreateSpan(start), attr.Value, children.ToArray());
314 | }
315 | else
316 | {
317 | SyntaxNode child = ParseNode(indentLevel);
318 | return new AttributeSingleNode(CreateSpan(start, attr), attr.Value, child);
319 | }
320 | }
321 |
322 | private FunctionCallNode ParseFunctionCall()
323 | {
324 | Token start = Take(TokenKind.DollarSign);
325 | Token id = Take(TokenKind.Identifier);
326 |
327 | return new FunctionCallNode(CreateSpan(start, id), id.Value);
328 | }
329 |
330 | private CallbackNode ParseCallback()
331 | {
332 | Token start = Take(TokenKind.Ampersand);
333 | Token id = Take(TokenKind.Identifier);
334 |
335 | return new CallbackNode(CreateSpan(start, id), id.Value);
336 | }
337 |
338 | private ConstantNode ParseConstant()
339 | {
340 | ConstantKind kind;
341 | switch (_current.Kind)
342 | {
343 | case TokenKind.BooleanLiteral:
344 | kind = ConstantKind.Boolean;
345 | break;
346 |
347 | case TokenKind.StringLiteral:
348 | kind = ConstantKind.String;
349 | break;
350 |
351 | case TokenKind.IntegerLiteral:
352 | kind = ConstantKind.Integer;
353 | break;
354 |
355 | case TokenKind.FloatLiteral:
356 | kind = ConstantKind.Float;
357 | break;
358 |
359 | case TokenKind.Identifier when _current.Value == "null":
360 | kind = ConstantKind.Null;
361 | break;
362 |
363 | default:
364 | throw UnexpectedToken("Constant");
365 | }
366 |
367 | var token = Take();
368 | var node = new ConstantNode(token.Span, token.Value, kind);
369 | return node;
370 | }
371 | #endregion
372 |
373 | #region Utility
374 | private SourceSpan CreateSpan(Token start) => CreateSpan(start.Span.Start, _current.Span.End);
375 | private SourceSpan CreateSpan(SyntaxNode start) => CreateSpan(start.Span.Start, _current.Span.End);
376 | private SourceSpan CreateSpan(SourceLocation start) => CreateSpan(start, _current.Span.End);
377 | private SourceSpan CreateSpan(Token start, Token end) => CreateSpan(start.Span.Start, end.Span.End);
378 | private SourceSpan CreateSpan(SourceLocation start, SourceLocation end)
379 | {
380 | return new SourceSpan(start, end);
381 | }
382 | #endregion
383 |
384 | #region Error Handling
385 | private void AddError(Severity severity, string message, SourceSpan? span = null)
386 | {
387 | _errorSink.AddError(message, _sourceCode, severity, span ?? CreateSpan(_current));
388 | }
389 |
390 | private SyntaxException SyntaxError(Severity severity, string message, SourceSpan? span = null)
391 | {
392 | _error = true;
393 | AddError(severity, message, span);
394 | return new SyntaxException(message);
395 | }
396 |
397 | private SyntaxException UnexpectedToken(TokenKind expected) => UnexpectedToken(expected.ToString());
398 | private SyntaxException UnexpectedToken(TokenKind[] expecteds)
399 | {
400 | var sb = new StringBuilder();
401 | foreach (var e in expecteds)
402 | {
403 | if (sb.Length > 0) sb.Append(",");
404 | sb.Append(e.ToString());
405 | }
406 | return UnexpectedToken(sb.ToString());
407 | }
408 | private SyntaxException UnexpectedToken(string expected)
409 | {
410 | Advance();
411 | var value = string.IsNullOrEmpty(_last?.Value) ? _last?.Kind.ToString() : _last?.Value;
412 | string message = $"Unexpected '{value}'. Expected '{expected}'.";
413 |
414 | return SyntaxError(Severity.Error, message, _last?.Span);
415 | }
416 | #endregion
417 | }
418 | }
419 |
--------------------------------------------------------------------------------
/FluiParser/Language/Tokenizer/Tokenizer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace FluiParser.Language.Tokenizer
7 | {
8 | public sealed class Tokenizer
9 | {
10 | private static Lazy _inst = new Lazy();
11 | public static Tokenizer Instance => _inst.Value;
12 |
13 | private static readonly string[] _widgets = { "AbsorbPointer", "AlertDialog", "Align", "AnimatedBuilder", "AnimatedContainer", "AnimatedCrossFade",
14 | "AnimatedDefaultTextStyle", "AnimatedListState", "AnimatedModalBarrier", "AnimatedOpacity", "AnimatedPhysicalModel", "AnimatedPositioned",
15 | "AnimatedSize", "AnimatedWidget", "AnimatedWidgetBaseState", "Appbar", "AspectRatio", "AssetBundle", "BackdropFilter", "Baseline", "BottomNavigationBar",
16 | "BottomSheet", "ButtonBar", "Card", "Center", "Checkbox", "Chip", "ClipOval", "ClipPath", "ClipRect", "Column", "ConstrainedBox", "Container",
17 | "CupertinoActivityIndicator", "CupertinoAlertDialog", "CupertinoButton", "CupertinoDialog", "CupertinoDialogAction", "CupertinoFullscreenDialogTransition",
18 | "CupertinoNavigationBar", "CupertinoPageScaffold", "CupertinoPageTransition", "CupertinoPicker", "CupertinoSlider", "CupertinoSwitch", "CupertinoTabBar",
19 | "CupertinoTabScaffold", "CupertinoTabView", "CustomMultiChildLayout", "CustomPaint", "CustomScrollView", "CustomSingleChildLayout", "DataTable", "DecoratedBox",
20 | "DecoratedBoxTransition", "DefaultTextStyle", "Dismissible", "Divider", "DragTarget", "Draggable", "Drawer", "ExcludeSemantics", "ExpansionPanel",
21 | "FadeTransition", "FittedBox", "FlatButton", "FloatingActionButton", "Flow", "FlutterLogo", "Form", "FormField", "FractionalTranslation", "FractionallySizedBox",
22 | "FutureBuilder", "GestureDetector", "GridView", "Hero", "Icon", "IconButton", "IgnorePointer", "Image", "IndexedStack", "IntrinsicHeight", "IntrinsicWidth",
23 | "LayoutBuilder", "LimitedBox", "LinearProgressIndicator", "ListBody", "ListTile", "ListView", "LongPressDraggable", "MediaQuery", "MergeSemantics", "Navigator",
24 | "NestedScrollView", "NotificationListener", "Offstage", "Opacity", "OverflowBox", "Padding", "Placeholder", "PopupMenuButton", "PositionedTransition", "Radio",
25 | "RaisedButton", "RawImage", "RawKeyboardListener", "RefreshIndicator", "RichText", "RotatedBox", "RotationTransition", "Row", "Scaffold", "ScaleTransition",
26 | "ScrollConfiguration", "Scrollable", "Scrollbar", "Semantics", "SimpleDialog", "SingleChildScrollView", "SizeTransition", "SizedBox", "SizedOverflowBox",
27 | "SlideTransition", "Slider", "SnackBar", "Stack", "Stepper", "StreamBuilder", "Switch", "TabBar", "TabBarView", "Table", "Text", "TextField", "Theme",
28 | "Tooltip", "Transform", "Wrap" };
29 | private static readonly string[] _boolLiterals = { "true", "false" };
30 |
31 | private StringBuilder _builder;
32 | private ErrorSink _errorSink;
33 | private int _line;
34 | private int _column;
35 | private int _index;
36 | private SourceCode _sourceCode;
37 | private SourceLocation _tokenStart;
38 | private char _indentationType;
39 |
40 | public ErrorSink ErrorSink => _errorSink;
41 |
42 | private char _ch => _sourceCode[_index];
43 | private char _last => Peek(-1);
44 | private char _next => Peek(1);
45 |
46 | private char Peek(int ahead) => _sourceCode[_index + ahead];
47 |
48 | public Tokenizer() : this(new ErrorSink()) { }
49 | public Tokenizer(ErrorSink errorSink)
50 | {
51 | _errorSink = errorSink;
52 | _builder = new StringBuilder();
53 | _sourceCode = null;
54 | }
55 |
56 | public IEnumerable TokenizeFile(string sourceCode) => TokenizeFile(new SourceCode(sourceCode));
57 | public IEnumerable TokenizeFile(SourceCode sourceCode)
58 | {
59 | _sourceCode = sourceCode;
60 | _builder.Clear();
61 | _errorSink.Clear();
62 | _tokenStart = new SourceLocation();
63 | _line = 1;
64 | _column = 0;
65 | _index = 0;
66 | _indentationType = '\0';
67 | CreateToken(TokenKind.EndOfFile);
68 |
69 | return TokenizeContents();
70 | }
71 |
72 | private void Advance()
73 | {
74 | _index++;
75 | _column++;
76 | }
77 |
78 | private void Consume()
79 | {
80 | _builder.Append(_ch);
81 | Advance();
82 | }
83 |
84 | private void DoNewLine()
85 | {
86 | _line++;
87 | _column = 0;
88 | }
89 |
90 | private Token CreateToken(TokenKind kind)
91 | {
92 | string contents = _builder.ToString();
93 | SourceLocation start = _tokenStart;
94 | SourceLocation end = new SourceLocation(_index, _line, _column);
95 |
96 | _tokenStart = end;
97 | _builder.Clear();
98 |
99 | return new Token(kind, contents, start, end);
100 | }
101 |
102 | private bool IsLetter => Char.IsLetter(_ch);
103 | private bool IsDigit => Char.IsDigit(_ch);
104 | private bool IsLetterOrDigit => Char.IsLetterOrDigit(_ch);
105 | private bool IsEOF => _ch == '\0';
106 | private bool IsIdentifier => IsLetterOrDigit || _ch == '_';
107 | private bool IsNewLine => _ch == '\r' || _ch == '\n';
108 | private bool IsWhiteSpace => (Char.IsWhiteSpace(_ch) || IsEOF) && !IsNewLine;
109 | private bool IsPunctuation => "<>{}()[]!$%^&*+-=/.,?;:|~@#".Contains(_ch);
110 | private bool IsWidget => _widgets.Contains(_builder.ToString());
111 | private bool IsBoolLiteral => _boolLiterals.Contains(_builder.ToString());
112 |
113 | private IEnumerable TokenizeContents()
114 | {
115 | while (!IsEOF)
116 | {
117 | yield return GetNextToken();
118 | }
119 |
120 | yield return CreateToken(TokenKind.EndOfFile);
121 | }
122 |
123 | private Token GetNextToken()
124 | {
125 | if (IsEOF)
126 | {
127 | return CreateToken(TokenKind.EndOfFile);
128 | }
129 | else if (IsNewLine)
130 | {
131 | return ScanNewLine();
132 | }
133 | else if (IsWhiteSpace)
134 | {
135 | return ScanWhiteSpace();
136 | }
137 | else if (IsDigit || (_ch == '-' && _next >= '0' && _next <= '9'))
138 | {
139 | return ScanInteger();
140 | }
141 | else if (_ch == '!')
142 | {
143 | return ScanComment();
144 | }
145 | else if (IsLetter || _ch == '_')
146 | {
147 | return ScanIdentifier();
148 | }
149 | else if (_ch == '\'' || _ch == '"')
150 | {
151 | return ScanStringLiteral();
152 | }
153 | else if (IsPunctuation)
154 | {
155 | return ScanPunctuation();
156 | }
157 | else
158 | {
159 | return ScanWord();
160 | }
161 | }
162 |
163 | private Token ScanNewLine()
164 | {
165 | while (IsNewLine)
166 | {
167 | Consume();
168 | }
169 |
170 | DoNewLine();
171 |
172 | return CreateToken(TokenKind.NewLine);
173 | }
174 |
175 | private Token ScanWhiteSpace()
176 | {
177 | if (_column == 0 && IsIndentation)
178 | {
179 | return ScanIndentation();
180 | }
181 |
182 | while (IsWhiteSpace)
183 | {
184 | Consume();
185 | }
186 |
187 | return CreateToken(TokenKind.WhiteSpace);
188 | }
189 |
190 | private bool IsIndentation => _ch == '\t' || _ch == ' ';
191 | private Token ScanIndentation()
192 | {
193 | if (_indentationType == '\0')
194 | {
195 | _indentationType = _ch;
196 |
197 | }
198 | else if (_ch != _indentationType)
199 | {
200 | AddError($"Inconsistent indentation character, {(_indentationType == '\t' ? "tab" : "space")} expected.", Severity.Warning);
201 | }
202 |
203 | while (IsIndentation)
204 | {
205 | Consume();
206 | }
207 |
208 | return CreateToken(TokenKind.Indentation);
209 | }
210 |
211 | private bool IsHexDigit => IsDigit || (Char.ToUpper(_ch) >= 'A' && Char.ToUpper(_ch) <= 'F');
212 | private Token ScanInteger()
213 | {
214 | if (_ch == '-')
215 | {
216 | Consume();
217 | }
218 |
219 | while (IsDigit)
220 | {
221 | Consume();
222 | }
223 |
224 | if (_ch == '.' || _ch == 'e')
225 | {
226 | return ScanFloat();
227 | }
228 | else if (_ch == 'x' && _last == '0')
229 | {
230 | Consume();
231 | while (IsHexDigit)
232 | {
233 | Consume();
234 | }
235 | }
236 |
237 | if (!IsWhiteSpace && !IsNewLine && !IsPunctuation && !IsEOF)
238 | {
239 | return ScanWord();
240 | }
241 |
242 | return CreateToken(TokenKind.IntegerLiteral);
243 | }
244 |
245 | private Token ScanFloat()
246 | {
247 | int preDotLength = _index - _tokenStart.Index;
248 |
249 | if (_ch == '.')
250 | {
251 | Consume();
252 |
253 | while (IsDigit)
254 | {
255 | Consume();
256 | }
257 |
258 | if (_last == '.')
259 | {
260 | ScanWord(message: "Must contain digits after '.'");
261 | }
262 | }
263 | else if (_ch == 'e')
264 | {
265 | Consume();
266 | if (_ch == '-')
267 | {
268 | Consume();
269 | }
270 | while (IsDigit)
271 | {
272 | Consume();
273 | }
274 |
275 | if (_last == 'e')
276 | {
277 | ScanWord(message: "Must contain digits after 'e'");
278 | }
279 | else if (_last == '-')
280 | {
281 | ScanWord(message: "Must contain digits after '-'");
282 | }
283 | }
284 |
285 | if (!IsWhiteSpace && !IsNewLine && !IsPunctuation && !IsEOF)
286 | {
287 | if (IsLetter)
288 | {
289 | return ScanWord(message: "'{0}' is an invalid float value.");
290 | }
291 |
292 | return ScanWord();
293 | }
294 |
295 | return CreateToken(TokenKind.FloatLiteral);
296 | }
297 |
298 | private Token ScanComment()
299 | {
300 | Consume();
301 | if (_ch == '!')
302 | {
303 | return ScanBlockComment();
304 | }
305 |
306 | Consume();
307 |
308 | while (!IsNewLine && !IsEOF)
309 | {
310 | Consume();
311 | }
312 |
313 | return CreateToken(TokenKind.LineComment);
314 | }
315 |
316 | private bool IsEndOfBlockComment => _ch == '!' && _next == '!';
317 | private Token ScanBlockComment()
318 | {
319 | while (!IsEndOfBlockComment)
320 | {
321 | if (IsEOF)
322 | {
323 | return CreateToken(TokenKind.Error);
324 | }
325 | if (IsNewLine)
326 | {
327 | do
328 | {
329 | Consume();
330 | } while (IsNewLine);
331 |
332 | DoNewLine();
333 | }
334 |
335 | Consume();
336 | }
337 |
338 | Consume();
339 | Consume();
340 |
341 | return CreateToken(TokenKind.BlockComment);
342 | }
343 |
344 | private Token ScanIdentifier()
345 | {
346 | while (IsIdentifier)
347 | {
348 | Consume();
349 | }
350 |
351 | if (!IsWhiteSpace && !IsNewLine && !IsPunctuation && !IsEOF)
352 | {
353 | return ScanWord();
354 | }
355 |
356 | if (IsBoolLiteral)
357 | {
358 | return CreateToken(TokenKind.BooleanLiteral);
359 | }
360 | else if (IsWidget)
361 | {
362 | return CreateToken(TokenKind.Widget);
363 | }
364 |
365 | return CreateToken(TokenKind.Identifier);
366 | }
367 |
368 | private Token ScanStringLiteral()
369 | {
370 | char quoteType = _ch;
371 |
372 | Advance();
373 |
374 | while (_ch != quoteType)
375 | {
376 | if (IsEOF)
377 | {
378 | AddError("Unexpected end of file", Severity.Fatal);
379 | return CreateToken(TokenKind.Error);
380 | }
381 | if (IsNewLine)
382 | {
383 | AddError("Unexpected line break in string literal", Severity.Error);
384 | return CreateToken(TokenKind.Error);
385 | }
386 |
387 | Consume();
388 | }
389 |
390 | Advance();
391 |
392 | return CreateToken(TokenKind.StringLiteral);
393 | }
394 |
395 | private Token ScanPunctuation()
396 | {
397 | switch (_ch)
398 | {
399 | case ':':
400 | Consume();
401 | return CreateToken(TokenKind.Colon);
402 | case '.':
403 | Consume();
404 | return CreateToken(TokenKind.Dot);
405 | case ',':
406 | Consume();
407 | return CreateToken(TokenKind.Comma);
408 | case '$':
409 | Consume();
410 | return CreateToken(TokenKind.DollarSign);
411 | case '@':
412 | Consume();
413 | return CreateToken(TokenKind.Ampersand);
414 | default:
415 | return ScanWord();
416 | }
417 | }
418 |
419 | private Token ScanWord(Severity severity = Severity.Error, string message = "Unexpected token '{0}'")
420 | {
421 | while (!IsWhiteSpace && !IsNewLine && !IsEOF && !IsPunctuation)
422 | {
423 | Consume();
424 | }
425 |
426 | AddError(string.Format(message, _builder.ToString()), severity);
427 | return CreateToken(TokenKind.Error);
428 | }
429 |
430 | private void AddError(string message, Severity severity)
431 | {
432 | var span = new SourceSpan(_tokenStart, new SourceLocation(_index, _line, _column));
433 | _errorSink.AddError(message, _sourceCode, severity, span);
434 | }
435 | }
436 | }
437 |
--------------------------------------------------------------------------------