From 4564d5446f123829a358fd67029e997f38d220ff Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Wed, 19 Oct 2022 01:16:54 +0200 Subject: [PATCH] Syntax coloration first attempt! Currently, when there are two many styles, the text is *slightly* offset from the normal one. --- .../Setting/ExpressionEditor.qml | 75 ++++++++++++++++++- .../LogarithmPlotter/js/parsing/builder.js | 4 +- .../LogarithmPlotter/js/parsing/common.js | 2 +- .../LogarithmPlotter/js/parsing/tokenizer.js | 73 ++++++++++++------ .../eu/ad5001/LogarithmPlotter/js/utils.js | 4 + 5 files changed, 131 insertions(+), 27 deletions(-) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml index 313b071..11a191a 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml @@ -21,6 +21,8 @@ import QtQuick 2.12 import QtQuick.Dialogs 1.3 as D import eu.ad5001.LogarithmPlotter.Popup 1.0 as Popup import "../js/mathlib.js" as MathLib +import "../js/utils.js" as Utils +import "../js/parsing/parsing.js" as Parsing /*! @@ -77,6 +79,7 @@ Item { /*! \qmlproperty string ExpressionEditor::openAndCloseMatches Characters that when pressed, should be immediately followed up by their closing character. + TODO: Make it configurable. */ readonly property var openAndCloseMatches: { "(": ")", @@ -85,6 +88,21 @@ Item { '"': '"' } + /*! + \qmlproperty string ExpressionEditor::colorScheme + Color scheme of the editor, currently based on Breeze Light. + TODO: Make it configurable. + */ + readonly property var colorScheme: { + 'NORMAL': "#1F1C1B", + 'VARIABLE': "#0057AE", + 'CONSTANT': "#5E2F00", + 'FUNCTION': "#644A9B", + 'OPERATOR': "#A44EA4", + 'STRING': "#9C0E0E", + 'NUMBER': "#805C00" + } + Icon { id: iconLabel anchors.top: parent.top @@ -117,6 +135,7 @@ Item { } } + TextField { id: editor anchors.top: parent.top @@ -126,8 +145,9 @@ Item { height: parent.height verticalAlignment: TextInput.AlignVCenter horizontalAlignment: control.label == "" ? TextInput.AlignLeft : TextInput.AlignHCenter + font.pixelSize: 14 text: control.defValue - color: sysPalette.windowText + color: "transparent"//sysPalette.windowText focus: true selectByMouse: true @@ -154,6 +174,18 @@ Item { event.accepted = true } } + + Text { + id: colorizedEditor + anchors.fill: editor + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: control.label == "" ? TextInput.AlignLeft : TextInput.AlignHCenter + textFormat: Text.StyledText + text: colorize(editor.text) + color: sysPalette.windowText + font.pixelSize: parent.font.pixelSize + //opacity: editor.activeFocus ? 0 : 1 + } } Button { @@ -210,5 +242,46 @@ Item { } return expr } + + /*! + \qmlmethod var ExpressionEditor::colorize(string expressionText) + Creates an HTML colorized string of the incomplete \c expressionText. + Returns the colorized and escaped expression if possible, null otherwise.. + */ + function colorize(text) { + let tokenizer = new Parsing.Tokenizer(new Parsing.Input(text), true, false) + let parsedText = "" + let token + console.log("Parsing text:", parsedText) + while((token = tokenizer.next()) != null) { + switch(token.type) { + case Parsing.TokenType.VARIABLE: + parsedText += `${token.value}` + break; + case Parsing.TokenType.CONSTANT: + parsedText += `${token.value}` + break; + case Parsing.TokenType.FUNCTION: + parsedText += `${token.value}` + break; + case Parsing.TokenType.OPERATOR: + parsedText += `${Utils.escapeHTML(token.value)}` + break; + case Parsing.TokenType.NUMBER: + parsedText += `${Utils.escapeHTML(token.value)}` + break; + case Parsing.TokenType.STRING: + parsedText += `${token.limitator}${Utils.escapeHTML(token.value)}${token.limitator}` + break; + case Parsing.TokenType.WHITESPACE: + case Parsing.TokenType.PUNCT: + default: + parsedText += Utils.escapeHTML(token.value).replace(/ /g, ' ') + break; + } + } + console.log("Parsed text:", parsedText) + return parsedText + } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js index c431fab..b81b6e1 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js @@ -18,8 +18,8 @@ .pragma library -import "ast.js" as AST -import "tokenizer.js" as TK +.import "ast.js" as AST +.import "tokenizer.js" as TK class ExpressionBuilder { diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/common.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/common.js index bda1b2c..05960fd 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/common.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/common.js @@ -33,7 +33,7 @@ class InputExpression { } skip(char) { - if(!atEnd() && peek() == char) { + if(!this.atEnd() && this.peek() == char) { this.position++; } else { this.raise("Unexpected character " + peek() + ". Expected character " + char); diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js index d600f5b..6d8e20b 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js @@ -22,20 +22,22 @@ const WHITESPACES = " \t\n\r" const STRING_LIMITORS = '"\'`'; -const OPERATORS = "+-*/^%"; -const PUNCTUTATION = "()[]{},"; +const OPERATORS = "+-*/^%?:=!><"; +const PUNCTUTATION = "()[]{},."; const NUMBER_CHARS = "0123456789." const IDENTIFIER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_₀₁₂₃₄₅₆₇₈₉αβγδεζηθκλμξρςστφχψωₐₑₒₓₔₕₖₗₘₙₚₛₜ" -enum TokenType { +var TokenType = { // Expression type - VARIABLE, - CONSTANT, - FUNCTION, - OPERATOR, - PUNCT, - NUMBER, - STRING + "WHITESPACE": "WHITESPACE", + "VARIABLE": "VARIABLE", + "CONSTANT": "CONSTANT", + "FUNCTION": "FUNCTION", + "OPERATOR": "OPERATOR", + "PUNCT": "PUNCT", + "NUMBER": "NUMBER", + "STRING": "STRING", + "UNKNOWN": "UNKNOWN" } class Token { @@ -46,9 +48,11 @@ class Token { } class ExpressionTokenizer { - constructor(input) { + constructor(input, tokenizeWhitespaces = false, errorOnUnknown = true) { this.input = input; this.currentToken = null; + this.tokenizeWhitespaces = tokenizeWhitespaces + this.errorOnUnknown = errorOnUnknown } skipWhitespaces() { @@ -56,6 +60,14 @@ class ExpressionTokenizer { this.input.next(); } + readWhitespaces() { + let included = ""; + while(!this.input.atEnd() && WHITESPACES.includes(this.input.peek())) { + included += this.input.next(); + } + return new Token(TokenType.WHITESPACE, included) + } + readString() { let delimitation = this.input.peek(); if(STRING_LIMITORS.includes(delimitation)) { @@ -68,7 +80,9 @@ class ExpressionTokenizer { included += this.input.next(); } this.input.skip(delimitation) - return new Token(TokenType.STRING, included); + let token = new Token(TokenType.STRING, included) + token.limitator = delimitation + return token } else { this.input.raise("Unexpected " + delimitation + ". Expected string delimitator") } @@ -84,12 +98,20 @@ class ExpressionTokenizer { } included += this.input.next(); } + return new Token(TokenType.NUMBER, included) + } + + readOperator() { + let included = ""; + while(!this.input.atEnd() && OPERATORS.includes(this.input.peek())) { + included += this.input.next(); + } + return new Token(TokenType.OPERATOR, included) } readIdentifier() { let identifier = ""; - let hasDot = false; - while(!this.input.atEnd() && IDENTIFIER_CHARS.includes(this.input.peek())) { + while(!this.input.atEnd() && IDENTIFIER_CHARS.includes(this.input.peek().toLowerCase())) { identifier += this.input.next(); } if(Reference.CONSTANTS_LIST.includes(identifier.toLowerCase())) { @@ -102,16 +124,21 @@ class ExpressionTokenizer { } readNextToken() { - this.skipWhitespaces() - if(input.atEnd()) return null; - let c = input.peek(); + if(!this.tokenizeWhitespaces) + this.skipWhitespaces() + if(this.input.atEnd()) return null; + let c = this.input.peek(); + if(this.tokenizeWhitespaces && WHITESPACES.includes(c)) return this.readWhitespaces(); if(STRING_LIMITORS.includes(c)) return this.readString(); if(NUMBER_CHARS.includes(c)) return this.readNumber(); - if(IDENTIFIER_CHARS.includes(c)) return this.readIdentifier(); + if(IDENTIFIER_CHARS.includes(c.toLowerCase())) return this.readIdentifier(); + if(OPERATORS.includes(c)) return this.readOperator(); if(Reference.CONSTANTS_LIST.includes(c)) return new Token(TokenType.CONSTANT, c); - if(OPERATORS.includes(c)) return new Token(TokenType.OPERATOR, c); - if(PUNCTUTATION.includes(c)) return new Token(TokenType.PUNCT, c); - this.input.throw("Unknown token character " + c) + if(PUNCTUTATION.includes(c)) return new Token(TokenType.PUNCT, this.input.next()); + if(this.errorOnUnknown) + this.input.throw("Unknown token character " + c) + else + return new Token(TokenType.UNKNOWN, this.input.next()); } peek() { @@ -134,8 +161,8 @@ class ExpressionTokenizer { } skip(type) { - Token next = Next(); + let next = this.next(); if(next.type != type) - input.raise("Unexpected token " + next.type.oLowerCase() + ' "' + next.value + '". Expected ' + type.toLowerCase()); + input.raise("Unexpected token " + next.type.toLowerCase() + ' "' + next.value + '". Expected ' + type.toLowerCase()); } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/utils.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/utils.js index a383846..ed3a8f5 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/utils.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/utils.js @@ -347,3 +347,7 @@ function getRandomColor() { } return color; } + +function escapeHTML(str) { + return str.replace(/&/g,'&').replace(//g,'>') ; +}