├── 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 | --------------------------------------------------------------------------------