resolve template string
This commit is contained in:
parent
80df04d3f6
commit
a144909fbf
6 changed files with 246 additions and 91 deletions
|
|
@ -21,9 +21,9 @@
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tsc",
|
"build": "npx tsc",
|
||||||
"build-watch": "npx tsc --watch",
|
"dev": "npx tsc --watch",
|
||||||
"test": "bun test/app.ts",
|
"test": "bun test/app.ts",
|
||||||
"test-watch": "bun --watch test/app.ts",
|
"test:watch": "bun --watch test/app.ts",
|
||||||
"prefetch": "bun scripts/prefetch"
|
"prefetch": "bun scripts/prefetch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ FuntionMap.set("sqrt", number => {
|
||||||
$1 = RandomBindingString(16),
|
$1 = RandomBindingString(16),
|
||||||
$2 = RandomBindingString(16)
|
$2 = RandomBindingString(16)
|
||||||
|
|
||||||
const { genBindings: absValue, value: absRtn } = callFn("abs")
|
const { genBindings: absValue, value: absRtn } = callFn("abs", number)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
genBindings: [
|
genBindings: [
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { makeToken, TokenKind, Token } from "./types.js"
|
import { makeToken, TokenKind, Token, TSToken, TSTokenKind } from "./types.js"
|
||||||
import * as Checker from "./Checker.js"
|
import * as Checker from "./Checker.js"
|
||||||
|
|
||||||
export function Lexer(input: string) {
|
export function Lexer(input: string, start: number = 0, end?: number) {
|
||||||
const tokens: Token[] = []
|
const tokens: Token[] = []
|
||||||
|
|
||||||
if (input.length === 0) return tokens
|
if (input.length === 0) return tokens
|
||||||
|
|
||||||
let index = 0
|
let index = start
|
||||||
do {
|
do {
|
||||||
const token = input[index]
|
const token = input[index]
|
||||||
|
|
||||||
|
|
@ -43,31 +43,88 @@ export function Lexer(input: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "`": {
|
case "`": {
|
||||||
const start = index++,
|
const tsTokens: TSToken[] = []
|
||||||
struct: boolean[] = []
|
const start = index
|
||||||
|
|
||||||
do {
|
const tokenization = (start: number) => {
|
||||||
const token = input[index]
|
while (index < input.length) {
|
||||||
let lastStruct = struct.lastItem()
|
const char = input[index]
|
||||||
|
if (char === "`") {
|
||||||
if (token === "`") {
|
|
||||||
if (struct.length) {
|
|
||||||
if (lastStruct === false) struct.pop()
|
|
||||||
else struct.push(false)
|
|
||||||
} else break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token === "$") {
|
|
||||||
if (input[index + 1] === "{" && !lastStruct) {
|
|
||||||
struct.push(true)
|
|
||||||
index++
|
index++
|
||||||
|
eatString()
|
||||||
|
} else if (char === "}") {
|
||||||
|
tsTokens.push({
|
||||||
|
kind: TSTokenKind.EXPRESSION,
|
||||||
|
tokens: Lexer(input, start + 1, index),
|
||||||
|
})
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
index++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (token === "}" && lastStruct === true) struct.pop()
|
const stringification = (start: number) => {
|
||||||
} while (++index < input.length)
|
while (index < input.length) {
|
||||||
|
const char = input[index]
|
||||||
|
if (char === "`") {
|
||||||
|
if (start + 1 !== index)
|
||||||
|
tsTokens.push({
|
||||||
|
kind: TSTokenKind.STRING,
|
||||||
|
tokens: {
|
||||||
|
kind: TokenKind.STRING,
|
||||||
|
start: start + 1,
|
||||||
|
length: index - start + 1,
|
||||||
|
value: `'${input.slice(start + 1, index)}'`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
tokens.push(makeToken(input, TokenKind.TEMPLATE_STRING, start, index - start + 1))
|
break
|
||||||
|
} else if (char === "$" && input[index + 1] === "{") {
|
||||||
|
tsTokens.push({
|
||||||
|
kind: TSTokenKind.STRING,
|
||||||
|
tokens: {
|
||||||
|
value: `'${input.slice(start + 1, index)}'`,
|
||||||
|
kind: TokenKind.STRING,
|
||||||
|
length: index - start + 1,
|
||||||
|
start,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tokenization(++index)
|
||||||
|
start = index
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eatString = () => {
|
||||||
|
while (index < input.length) {
|
||||||
|
const char = input[index]
|
||||||
|
|
||||||
|
if (char === "`") {
|
||||||
|
break
|
||||||
|
} else if (char === "$" && input[index + 1] === "{") {
|
||||||
|
index++
|
||||||
|
eatTemplate()
|
||||||
|
}
|
||||||
|
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eatTemplate = () => {
|
||||||
|
while (index < input.length) {
|
||||||
|
const char = input[index]
|
||||||
|
if (char === "`") {
|
||||||
|
eatString()
|
||||||
|
} else if (char === "}") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stringification(index++)
|
||||||
|
tokens.push(makeToken(tsTokens, TokenKind.TEMPLATE_STRING, start, index - start + 1))
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -117,10 +174,15 @@ export function Lexer(input: string) {
|
||||||
} else if (Checker.isWordChar(token)) {
|
} else if (Checker.isWordChar(token)) {
|
||||||
while (Checker.isWordChar(input[index + 1])) index++
|
while (Checker.isWordChar(input[index + 1])) index++
|
||||||
tokens.push(makeToken(input, TokenKind.WORD, start, index - start + 1))
|
tokens.push(makeToken(input, TokenKind.WORD, start, index - start + 1))
|
||||||
|
} else if (!Checker.isBlankChar(token)) {
|
||||||
|
console.error(
|
||||||
|
`\x1b[31m${input.slice(0, index)}>>>${token}<<<${input.slice(index + 1)}\nInvalid character.\x1b[0m`,
|
||||||
|
)
|
||||||
|
throw new Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (++index < input.length)
|
} while (++index < (end || input.length))
|
||||||
|
|
||||||
return tokens
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { BindingType } from "../../types/enums/BindingType.js"
|
||||||
import { BindingItem } from "../../types/properties/value.js"
|
import { BindingItem } from "../../types/properties/value.js"
|
||||||
import { FuntionMap } from "./Funtion.js"
|
import { FuntionMap } from "./Funtion.js"
|
||||||
import { Lexer } from "./Lexer.js"
|
import { Lexer } from "./Lexer.js"
|
||||||
import { Expression, GenBinding, Token, TokenKind } from "./types.js"
|
import { Expression, GenBinding, Token, TokenKind, TSToken, TSTokenKind } from "./types.js"
|
||||||
|
|
||||||
export class Parser {
|
export class Parser {
|
||||||
position: number = 0
|
position: number = 0
|
||||||
|
|
@ -29,7 +29,74 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseExpression(): Expression {
|
private parseExpression(): Expression {
|
||||||
return this.parseAdditiveExpression()
|
return this.parseOrExpression()
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseOrExpression(): Expression {
|
||||||
|
let left = this.parseAndExpression(),
|
||||||
|
current
|
||||||
|
|
||||||
|
while (
|
||||||
|
(current = this.at()) &&
|
||||||
|
this.at().kind === TokenKind.OPERATOR &&
|
||||||
|
["||"].includes(<string>current.value)
|
||||||
|
) {
|
||||||
|
this.eat()
|
||||||
|
left = `(${left} or ${this.parseAndExpression()})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAndExpression(): Expression {
|
||||||
|
let left = this.parseComparisonExpression(),
|
||||||
|
current
|
||||||
|
|
||||||
|
while (
|
||||||
|
(current = this.at()) &&
|
||||||
|
this.at().kind === TokenKind.OPERATOR &&
|
||||||
|
["&&"].includes(<string>current.value)
|
||||||
|
) {
|
||||||
|
this.eat()
|
||||||
|
left = `(${left} and ${this.parseComparisonExpression()})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseComparisonExpression(): Expression {
|
||||||
|
let left = this.parseAdditiveExpression(),
|
||||||
|
current
|
||||||
|
|
||||||
|
while (
|
||||||
|
(current = this.at()) &&
|
||||||
|
this.at().kind === TokenKind.OPERATOR &&
|
||||||
|
["==", ">", "<", ">=", "<=", "!="].includes(<string>current.value)
|
||||||
|
) {
|
||||||
|
const operator = this.eat()
|
||||||
|
const right = this.parseAdditiveExpression()
|
||||||
|
|
||||||
|
switch (operator.value) {
|
||||||
|
case "==":
|
||||||
|
left = `(${left} = ${right})`
|
||||||
|
break
|
||||||
|
|
||||||
|
case "!=":
|
||||||
|
left = `(not (${left} = ${right}))`
|
||||||
|
break
|
||||||
|
|
||||||
|
case ">=":
|
||||||
|
case "<=":
|
||||||
|
left = `((${left} ${operator.value[0]} ${right}) or (${left} = ${right}))`
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
left = `(${left} ${operator.value} ${right})`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAdditiveExpression(): Expression {
|
private parseAdditiveExpression(): Expression {
|
||||||
|
|
@ -39,37 +106,12 @@ export class Parser {
|
||||||
while (
|
while (
|
||||||
(current = this.at()) &&
|
(current = this.at()) &&
|
||||||
this.at()?.kind === TokenKind.OPERATOR &&
|
this.at()?.kind === TokenKind.OPERATOR &&
|
||||||
["+", "-", "==", ">", "<", ">=", "<=", "!=", "&&", "||"].includes(current.value)
|
["+", "-"].includes(<string>current.value)
|
||||||
) {
|
) {
|
||||||
const operator = this.eat()
|
const operator = this.eat()
|
||||||
const right = this.parseMultiplicativeExpression()
|
const right = this.parseMultiplicativeExpression()
|
||||||
|
|
||||||
switch (operator.value) {
|
left = `(${left} ${operator.value} ${right})`
|
||||||
case "==":
|
|
||||||
left = `(${left} = ${right})`
|
|
||||||
break
|
|
||||||
|
|
||||||
case ">=":
|
|
||||||
case "<=":
|
|
||||||
left = `((${left} ${operator.value[0]} ${right}) or (${left} = ${right}))`
|
|
||||||
break
|
|
||||||
|
|
||||||
case "!=":
|
|
||||||
left = `(not (${left} = ${right}))`
|
|
||||||
break
|
|
||||||
|
|
||||||
case "&&":
|
|
||||||
left = `(${left} and ${right})`
|
|
||||||
break
|
|
||||||
|
|
||||||
case "||":
|
|
||||||
left = `(${left} or ${right})`
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
left = `(${left} ${operator.value} ${right})`
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return left
|
return left
|
||||||
|
|
@ -82,7 +124,7 @@ export class Parser {
|
||||||
while (
|
while (
|
||||||
(current = this.at()) &&
|
(current = this.at()) &&
|
||||||
this.at()?.kind === TokenKind.OPERATOR &&
|
this.at()?.kind === TokenKind.OPERATOR &&
|
||||||
["*", "/", "%"].includes(current.value)
|
["*", "/", "%"].includes(<string>current.value)
|
||||||
) {
|
) {
|
||||||
const operator = this.eat()
|
const operator = this.eat()
|
||||||
const right = this.parsePrimaryExpression()
|
const right = this.parsePrimaryExpression()
|
||||||
|
|
@ -99,7 +141,7 @@ export class Parser {
|
||||||
|
|
||||||
switch (left?.kind) {
|
switch (left?.kind) {
|
||||||
case TokenKind.WORD:
|
case TokenKind.WORD:
|
||||||
return this.parseCallExpression(this.eat())
|
return this.parseCallableOrLiteral(this.eat())
|
||||||
|
|
||||||
case TokenKind.OPEN_PARENTHESIS: {
|
case TokenKind.OPEN_PARENTHESIS: {
|
||||||
this.eat()
|
this.eat()
|
||||||
|
|
@ -109,6 +151,32 @@ export class Parser {
|
||||||
return `(${value})`
|
return `(${value})`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case TokenKind.VARIABLE:
|
||||||
|
case TokenKind.NUMBER:
|
||||||
|
case TokenKind.STRING:
|
||||||
|
return <string>this.eat().value
|
||||||
|
|
||||||
|
case TokenKind.TEMPLATE_STRING:
|
||||||
|
return `(${(<TSToken[]>this.eat().value)
|
||||||
|
.map(v => {
|
||||||
|
if (v.kind === TSTokenKind.STRING) return v.tokens.value
|
||||||
|
else {
|
||||||
|
const bakTokens = this.tokens
|
||||||
|
const bakPosition = this.position
|
||||||
|
|
||||||
|
this.tokens = v.tokens
|
||||||
|
this.position = 0
|
||||||
|
|
||||||
|
const out = this.parseExpression()
|
||||||
|
|
||||||
|
this.tokens = bakTokens
|
||||||
|
this.position = bakPosition
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(" + ")})`
|
||||||
|
|
||||||
case TokenKind.OPERATOR: {
|
case TokenKind.OPERATOR: {
|
||||||
if (left.value === "-" || left.value === "+") {
|
if (left.value === "-" || left.value === "+") {
|
||||||
this.eat()
|
this.eat()
|
||||||
|
|
@ -144,35 +212,35 @@ export class Parser {
|
||||||
|
|
||||||
const value = this.parsePrimaryExpression()
|
const value = this.parsePrimaryExpression()
|
||||||
return not ? `(not ${value})` : value
|
return not ? `(not ${value})` : value
|
||||||
} else break
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case TokenKind.VARIABLE:
|
default:
|
||||||
case TokenKind.NUMBER:
|
console.log(left)
|
||||||
case TokenKind.STRING:
|
|
||||||
return this.eat().value
|
|
||||||
|
|
||||||
case undefined:
|
|
||||||
this.expect(TokenKind.NUMBER, "Unexpected token!")
|
this.expect(TokenKind.NUMBER, "Unexpected token!")
|
||||||
}
|
}
|
||||||
|
|
||||||
return left.value
|
return <string>left.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseCallExpression(callerToken: Token): Expression {
|
private parseCallableOrLiteral(callerToken: Token): Expression {
|
||||||
const left = this.at()
|
const left = this.at()
|
||||||
|
|
||||||
if (left?.kind === TokenKind.OPEN_PARENTHESIS) {
|
if (left?.kind === TokenKind.OPEN_PARENTHESIS) {
|
||||||
this.eat()
|
this.eat()
|
||||||
const args: Expression[] = []
|
const args: Expression[] = []
|
||||||
|
|
||||||
|
if (this.at().kind === TokenKind.COMMA) {
|
||||||
|
this.expect(TokenKind.NUMBER, "Unexpected token!")
|
||||||
|
}
|
||||||
|
|
||||||
if (this.at().kind !== TokenKind.CLOSE_PARENTHESIS) {
|
if (this.at().kind !== TokenKind.CLOSE_PARENTHESIS) {
|
||||||
args.push(this.parseExpression())
|
args.push(this.parseExpression())
|
||||||
|
|
||||||
while (this.at().kind === TokenKind.COMMA) {
|
while (this.at().kind === TokenKind.COMMA) {
|
||||||
this.eat()
|
this.eat()
|
||||||
if (this.at().kind === TokenKind.CLOSE_PARENTHESIS) {
|
if (this.at().kind === TokenKind.CLOSE_PARENTHESIS) {
|
||||||
this.expect(TokenKind.CLOSE_PARENTHESIS, "Unexpected token!")
|
this.expect(TokenKind.NUMBER, "Unexpected token!")
|
||||||
}
|
}
|
||||||
args.push(this.parseExpression())
|
args.push(this.parseExpression())
|
||||||
}
|
}
|
||||||
|
|
@ -180,15 +248,18 @@ export class Parser {
|
||||||
|
|
||||||
this.eat()
|
this.eat()
|
||||||
|
|
||||||
return this.funtionCall(callerToken.value, ...args)
|
return this.funtionCall(<string>callerToken.value, ...args)
|
||||||
} else {
|
} else {
|
||||||
this.warn("This token should be a string!", callerToken)
|
this.warn(
|
||||||
return callerToken.value
|
`Implicit string literal '${callerToken.value}'. Use quoted string ('${callerToken.value}') for clarity!`,
|
||||||
|
callerToken,
|
||||||
|
)
|
||||||
|
return <string>callerToken.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private funtionCall(name: string, ...params: Expression[]): Expression {
|
private funtionCall(name: string, ...params: Expression[]): Expression {
|
||||||
const func = FuntionMap.get(name)
|
const func = FuntionMap.get(name.toLowerCase())
|
||||||
if (!func) {
|
if (!func) {
|
||||||
return this.expect(TokenKind.WORD, "Function not found!")!
|
return this.expect(TokenKind.WORD, "Function not found!")!
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -202,7 +273,7 @@ export class Parser {
|
||||||
const prev = this.at() || this.last()
|
const prev = this.at() || this.last()
|
||||||
if (!prev || prev.kind !== kind) {
|
if (!prev || prev.kind !== kind) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`\x1b[31m${this.getPointer(prev)}\n` + `[ERROR]: ${err}\x1b[0m - Expected ${TokenKind[kind]}`
|
`\x1b[31m${this.getPointer(prev)}\n` + `[ERROR]: ${err}\x1b[0m - Expected ${TokenKind[kind]}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +286,7 @@ export class Parser {
|
||||||
private getPointer(token: Token) {
|
private getPointer(token: Token) {
|
||||||
return `${this.input.slice(0, token.start)}>>>${this.input.slice(
|
return `${this.input.slice(0, token.start)}>>>${this.input.slice(
|
||||||
token.start,
|
token.start,
|
||||||
token.start + token.length
|
token.start + token.length,
|
||||||
)}<<<${this.input.slice(token.start + token.length)}`
|
)}<<<${this.input.slice(token.start + token.length)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,7 +299,7 @@ export class Parser {
|
||||||
binding_type: BindingType.VIEW,
|
binding_type: BindingType.VIEW,
|
||||||
source_property_name: `(${source})`,
|
source_property_name: `(${source})`,
|
||||||
target_property_name: `${target}`,
|
target_property_name: `${target}`,
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,27 +12,50 @@ export enum TokenKind {
|
||||||
COMMA,
|
COMMA,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupType {
|
export enum TSTokenKind {
|
||||||
FUNCTION_CALL,
|
STRING,
|
||||||
FUNCTION_PARAMS,
|
EXPRESSION,
|
||||||
OPERATOR_SCOPE,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Token {
|
export type TSToken =
|
||||||
kind: TokenKind
|
| {
|
||||||
value: string
|
kind: TSTokenKind.EXPRESSION
|
||||||
|
tokens: Token[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: TSTokenKind.STRING
|
||||||
|
tokens: Token
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseToken {
|
||||||
start: number
|
start: number
|
||||||
length: number
|
length: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NormalToken extends BaseToken {
|
||||||
|
value: string
|
||||||
|
kind: Exclude<TokenKind, TokenKind.TEMPLATE_STRING>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateToken extends BaseToken {
|
||||||
|
value: TSToken[]
|
||||||
|
kind: TokenKind.TEMPLATE_STRING
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Token = NormalToken | TemplateToken
|
||||||
|
|
||||||
export type Expression = string
|
export type Expression = string
|
||||||
|
|
||||||
export function makeToken(input: string, kind: TokenKind, start: number, length: number = 1): Token {
|
export function makeToken<T extends TokenKind>(
|
||||||
return {
|
input: T extends TokenKind.TEMPLATE_STRING ? TSToken[] : string,
|
||||||
value: input.slice(start, start + length),
|
kind: T,
|
||||||
kind,
|
start: number,
|
||||||
start,
|
length: number = 1,
|
||||||
length,
|
): Token {
|
||||||
|
if (kind === TokenKind.TEMPLATE_STRING) {
|
||||||
|
return { value: input as TSToken[], kind: kind, start, length }
|
||||||
|
} else {
|
||||||
|
return { value: input.slice(start, start + length) as string, kind, start, length }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Parser, Panel } from ".."
|
import { Lexer, Parser } from ".."
|
||||||
|
|
||||||
const { gen, out } = new Parser("abs(#a)").out()
|
const { out } = new Parser("`A${`#a${#a + #b}`}A`").out()
|
||||||
|
console.log(out)
|
||||||
console.log(gen, out)
|
|
||||||
|
|
|
||||||
Reference in a new issue