resolve template string

This commit is contained in:
Asaki Yuki 2026-01-17 18:04:48 +07:00
parent 80df04d3f6
commit a144909fbf
6 changed files with 246 additions and 91 deletions

View file

@ -21,9 +21,9 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "npx tsc",
"build-watch": "npx tsc --watch",
"dev": "npx tsc --watch",
"test": "bun test/app.ts",
"test-watch": "bun --watch test/app.ts",
"test:watch": "bun --watch test/app.ts",
"prefetch": "bun scripts/prefetch"
},
"devDependencies": {

View file

@ -34,7 +34,7 @@ FuntionMap.set("sqrt", number => {
$1 = RandomBindingString(16),
$2 = RandomBindingString(16)
const { genBindings: absValue, value: absRtn } = callFn("abs")
const { genBindings: absValue, value: absRtn } = callFn("abs", number)
return {
genBindings: [

View file

@ -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"
export function Lexer(input: string) {
export function Lexer(input: string, start: number = 0, end?: number) {
const tokens: Token[] = []
if (input.length === 0) return tokens
let index = 0
let index = start
do {
const token = input[index]
@ -43,31 +43,88 @@ export function Lexer(input: string) {
}
case "`": {
const start = index++,
struct: boolean[] = []
const tsTokens: TSToken[] = []
const start = index
do {
const token = input[index]
let lastStruct = struct.lastItem()
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)
const tokenization = (start: number) => {
while (index < input.length) {
const char = input[index]
if (char === "`") {
index++
eatString()
} else if (char === "}") {
tsTokens.push({
kind: TSTokenKind.EXPRESSION,
tokens: Lexer(input, start + 1, index),
})
break
}
index++
}
}
if (token === "}" && lastStruct === true) struct.pop()
} while (++index < input.length)
const stringification = (start: number) => {
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
}
@ -117,10 +174,15 @@ export function Lexer(input: string) {
} else if (Checker.isWordChar(token)) {
while (Checker.isWordChar(input[index + 1])) index++
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
}

View file

@ -2,7 +2,7 @@ import { BindingType } from "../../types/enums/BindingType.js"
import { BindingItem } from "../../types/properties/value.js"
import { FuntionMap } from "./Funtion.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 {
position: number = 0
@ -29,7 +29,74 @@ export class Parser {
}
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 {
@ -39,37 +106,12 @@ export class Parser {
while (
(current = this.at()) &&
this.at()?.kind === TokenKind.OPERATOR &&
["+", "-", "==", ">", "<", ">=", "<=", "!=", "&&", "||"].includes(current.value)
["+", "-"].includes(<string>current.value)
) {
const operator = this.eat()
const right = this.parseMultiplicativeExpression()
switch (operator.value) {
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
}
left = `(${left} ${operator.value} ${right})`
}
return left
@ -82,7 +124,7 @@ export class Parser {
while (
(current = this.at()) &&
this.at()?.kind === TokenKind.OPERATOR &&
["*", "/", "%"].includes(current.value)
["*", "/", "%"].includes(<string>current.value)
) {
const operator = this.eat()
const right = this.parsePrimaryExpression()
@ -99,7 +141,7 @@ export class Parser {
switch (left?.kind) {
case TokenKind.WORD:
return this.parseCallExpression(this.eat())
return this.parseCallableOrLiteral(this.eat())
case TokenKind.OPEN_PARENTHESIS: {
this.eat()
@ -109,6 +151,32 @@ export class Parser {
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: {
if (left.value === "-" || left.value === "+") {
this.eat()
@ -144,35 +212,35 @@ export class Parser {
const value = this.parsePrimaryExpression()
return not ? `(not ${value})` : value
} else break
}
}
case TokenKind.VARIABLE:
case TokenKind.NUMBER:
case TokenKind.STRING:
return this.eat().value
case undefined:
default:
console.log(left)
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()
if (left?.kind === TokenKind.OPEN_PARENTHESIS) {
this.eat()
const args: Expression[] = []
if (this.at().kind === TokenKind.COMMA) {
this.expect(TokenKind.NUMBER, "Unexpected token!")
}
if (this.at().kind !== TokenKind.CLOSE_PARENTHESIS) {
args.push(this.parseExpression())
while (this.at().kind === TokenKind.COMMA) {
this.eat()
if (this.at().kind === TokenKind.CLOSE_PARENTHESIS) {
this.expect(TokenKind.CLOSE_PARENTHESIS, "Unexpected token!")
this.expect(TokenKind.NUMBER, "Unexpected token!")
}
args.push(this.parseExpression())
}
@ -180,15 +248,18 @@ export class Parser {
this.eat()
return this.funtionCall(callerToken.value, ...args)
return this.funtionCall(<string>callerToken.value, ...args)
} else {
this.warn("This token should be a string!", callerToken)
return callerToken.value
this.warn(
`Implicit string literal '${callerToken.value}'. Use quoted string ('${callerToken.value}') for clarity!`,
callerToken,
)
return <string>callerToken.value
}
}
private funtionCall(name: string, ...params: Expression[]): Expression {
const func = FuntionMap.get(name)
const func = FuntionMap.get(name.toLowerCase())
if (!func) {
return this.expect(TokenKind.WORD, "Function not found!")!
} else {
@ -202,7 +273,7 @@ export class Parser {
const prev = this.at() || this.last()
if (!prev || prev.kind !== kind) {
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) {
return `${this.input.slice(0, token.start)}>>>${this.input.slice(
token.start,
token.start + token.length
token.start + token.length,
)}<<<${this.input.slice(token.start + token.length)}`
}
@ -228,7 +299,7 @@ export class Parser {
binding_type: BindingType.VIEW,
source_property_name: `(${source})`,
target_property_name: `${target}`,
}
},
),
}
}

View file

@ -12,27 +12,50 @@ export enum TokenKind {
COMMA,
}
export enum GroupType {
FUNCTION_CALL,
FUNCTION_PARAMS,
OPERATOR_SCOPE,
export enum TSTokenKind {
STRING,
EXPRESSION,
}
export interface Token {
kind: TokenKind
value: string
export type TSToken =
| {
kind: TSTokenKind.EXPRESSION
tokens: Token[]
}
| {
kind: TSTokenKind.STRING
tokens: Token
}
export interface BaseToken {
start: 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 function makeToken(input: string, kind: TokenKind, start: number, length: number = 1): Token {
return {
value: input.slice(start, start + length),
kind,
start,
length,
export function makeToken<T extends TokenKind>(
input: T extends TokenKind.TEMPLATE_STRING ? TSToken[] : string,
kind: T,
start: number,
length: number = 1,
): 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 }
}
}

View file

@ -1,5 +1,4 @@
import { Parser, Panel } from ".."
import { Lexer, Parser } from ".."
const { gen, out } = new Parser("abs(#a)").out()
console.log(gen, out)
const { out } = new Parser("`A${`#a${#a + #b}`}A`").out()
console.log(out)