Syntax coloration first attempt!
Currently, when there are two many styles, the text is *slightly* offset from the normal one.
This commit is contained in:
parent
32db56304b
commit
4564d5446f
5 changed files with 131 additions and 27 deletions
|
@ -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 += `<font color="${colorScheme.VARIABLE}">${token.value}</font>`
|
||||
break;
|
||||
case Parsing.TokenType.CONSTANT:
|
||||
parsedText += `<font color="${colorScheme.CONSTANT}">${token.value}</font>`
|
||||
break;
|
||||
case Parsing.TokenType.FUNCTION:
|
||||
parsedText += `<font color="${Utils.escapeHTML(colorScheme.FUNCTION)}">${token.value}</font>`
|
||||
break;
|
||||
case Parsing.TokenType.OPERATOR:
|
||||
parsedText += `<font color="${colorScheme.OPERATOR}">${Utils.escapeHTML(token.value)}</font>`
|
||||
break;
|
||||
case Parsing.TokenType.NUMBER:
|
||||
parsedText += `<font color="${colorScheme.NUMBER}">${Utils.escapeHTML(token.value)}</font>`
|
||||
break;
|
||||
case Parsing.TokenType.STRING:
|
||||
parsedText += `<font color="${colorScheme.STRING}">${token.limitator}${Utils.escapeHTML(token.value)}${token.limitator}</font>`
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -347,3 +347,7 @@ function getRandomColor() {
|
|||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') ;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue