+ return
+ {() => state.toastMessages.length ?
{() => state.toastMessages.map(item =>
-
{
+
{
state.toastMessages = state.toastMessages.filter(i => i !== item)
}}>{item.stack ?? item.message}
)}
-
- )
+
:
}
+
}
diff --git a/src/env.ts b/src/env.ts
index 2dd1b0c..862cae8 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -7,3 +7,9 @@ const Env = z.object({
export const env = Env.parse(Object.assign({
VITE_API_URL: location.origin
}, import.meta.env))
+
+const url = new URL(location.origin)
+if (url.port.length) {
+ url.port = '8000'
+ env.VITE_API_URL = url.href.slice(0, -1) // trim trailing slash
+}
diff --git a/src/lang/tokenize.test.ts b/src/lang/tokenize.test.ts
new file mode 100644
index 0000000..d75927f
--- /dev/null
+++ b/src/lang/tokenize.test.ts
@@ -0,0 +1,70 @@
+import { Token, tokenize } from '~/src/lang/tokenize.ts'
+
+describe('tokenize', () => {
+ it('simple', () => {
+ const source = { code: 'hello world\n123' }
+ const tokens = [...tokenize(source)]
+ expect(tokens.length).toBe(3)
+ expect(tokens[0]).toMatchObject({
+ type: Token.Type.Id,
+ text: 'hello',
+ line: 0,
+ col: 0,
+ right: 5,
+ bottom: 0,
+ index: 0,
+ length: 5,
+ source,
+ })
+ expect(tokens[1]).toMatchObject({
+ type: Token.Type.Id,
+ text: 'world',
+ line: 0,
+ col: 6,
+ right: 11,
+ bottom: 0,
+ index: 6,
+ length: 5,
+ source,
+ })
+ expect(tokens[2]).toMatchObject({
+ type: Token.Type.Number,
+ text: '123',
+ line: 1,
+ col: 0,
+ right: 3,
+ bottom: 1,
+ index: 12,
+ length: 3,
+ source,
+ })
+ })
+
+ it('multiline', () => {
+ const source = { code: '[; hello world\n123 ]' }
+ const tokens = [...tokenize(source)]
+ expect(tokens.length).toBe(2)
+ expect(tokens[0]).toMatchObject({
+ type: Token.Type.Comment,
+ text: '[; hello world',
+ line: 0,
+ col: 0,
+ right: 14,
+ bottom: 0,
+ index: 0,
+ length: 14,
+ source,
+ })
+ expect(tokens[1]).toMatchObject({
+ type: Token.Type.Comment,
+ text: '123 ]',
+ line: 1,
+ col: 0,
+ right: 5,
+ bottom: 1,
+ index: 15,
+ length: 5,
+ source,
+ })
+ })
+})
diff --git a/src/lang/tokenize.ts b/src/lang/tokenize.ts
new file mode 100644
index 0000000..c2a93ff
--- /dev/null
+++ b/src/lang/tokenize.ts
@@ -0,0 +1,233 @@
+export interface Source {
+ code: string
+}
+
+export interface Token
{
+ /** Token type. */
+ type: T
+ /** The text representation of the token. */
+ text: string
+ /** Zero based line number. */
+ line: number
+ /** Zero based column number. */
+ col: number
+ /** Rightmost column number, zero based. */
+ right: number
+ /** Bottom line, zero based. */
+ bottom: number
+ /** Zero based index. */
+ index: number
+ /** Token length. */
+ length: number
+ /** Reference to the source code document. */
+ source: Source
+}
+
+export namespace Token {
+ export enum Type {
+ Newline = 'Newline',
+ Realnewline = 'Realnewline',
+ Whitespace = 'Whitespace',
+ Native = 'Native',
+ String = 'String',
+ Keyword = 'Keyword',
+ Op = 'Op',
+ Id = 'Id',
+ Number = 'Number',
+ BlockComment = 'BlockComment',
+ Comment = 'Comment',
+ Any = 'Any',
+ }
+
+ export const typesEntries = [
+ [Type.Newline, /(\n)/],
+ [Type.Realnewline, /(\r)/],
+ [Type.Whitespace, /(\s+?)/],
+ [Type.Native, /(`[a-z]+`)/],
+ [Type.String, /('[^'\n]*['\n])/],
+ [Type.Keyword, /(M|S|\\|@|,)/],
+ [Type.Op, /(\?)/],
+ [Type.Id, /([a-zA-Z]+[a-zA-Z0-9_]*)/],
+ [Type.Number, /([0-9]+[dhk][0-9]*|[0-9]+\.[0-9]+|\.?[0-9]+)/],
+ [Type.BlockComment, /(\[;|\(;|{;)/],
+ [Type.Op, /(\*=|\+=|:=|\+|-|\*|\/|\^|%|!|=|\{|\}|\(|\)|\[|\]|:|\.)/],
+ [Type.Comment, /(;)/],
+ [Type.Any, /(.+?)/],
+ ] as const
+
+ export const regexp = new RegExp(
+ Array.from(typesEntries,
+ ([, regexp]) => regexp.source
+ ).join('|'),
+ 'g'
+ )
+
+ export interface Bounds {
+ line: number
+ col: number
+ right: number
+ bottom: number
+ index: number
+ length: number
+ }
+
+ export const Empty: Token = {
+ type: 0,
+ text: '',
+ line: 0,
+ col: 0,
+ right: 0,
+ bottom: 0,
+ index: 0,
+ length: 0,
+ source: { code: '' }
+ }
+
+ export const Close = {
+ '[': ']',
+ '(': ')',
+ '{': '}',
+ } as const
+
+ export function isTerminal(token: Token) {
+ switch (token.type) {
+ case Type.Any:
+ case Type.Comment: return false
+ }
+ return true
+ }
+
+ export function closeOf(token: Token) {
+ return Close[token.text as keyof typeof Close].charCodeAt(0)
+ }
+
+ export function bounds(tokens: Token[]): Bounds {
+ let line: number = Infinity
+ let col: number = Infinity
+ let right: number = 0
+ let index: number = Infinity
+ let bottom: number = 0
+ let end: number = 0
+
+ let t: { line: number, col: number, right: number, index: number, length: number }
+
+ for (const t of tokens) {
+ if (t.index < index) index = t.index
+ if (t.index + t.length > end) end = t.index + t.length
+ if (t.line < line) line = t.line
+ if (t.line > bottom) bottom = t.line
+ if (t.col < col && t.line === line) col = t.col
+ if (t.right > right) right = t.right
+ }
+
+ return { line, col, right, bottom, index, length: end - index }
+ }
+}
+
+export function* tokenize(source: Source): Generator {
+ const code = source.code
+ let i = 0
+ let text: string | null
+ let type: Token.Type
+ let line = 0
+ let lineIndex = 0
+ let depth = 0
+ let col = 0
+ let match: RegExpMatchArray
+ let begin: RegExpMatchArray
+ let slice: string
+ let open: keyof typeof Token.Close
+ let close: typeof Token.Close[keyof typeof Token.Close]
+ let commenting = false
+ const length = Token.typesEntries.length + 1
+ const it = code.matchAll(Token.regexp)
+ outer: for (match of it) {
+ for (i = 1; i < length; i++) {
+ text = match[i]
+ if (text != null) {
+ type = Token.typesEntries[i - 1][0]
+ switch (type) {
+ case Token.Type.Realnewline:
+ commenting = false
+ break
+ case Token.Type.Whitespace:
+ break
+ case Token.Type.Newline:
+ ++line
+ lineIndex = match.index! + 1
+ break
+ case Token.Type.BlockComment: {
+ type = Token.Type.Comment
+ begin = match
+ open = text[0] as keyof typeof Token.Close
+ close = Token.Close[open]
+ depth = 1
+ for (const m of it) {
+ for (let x = 0, t: string; x < length; x++) {
+ t = m[x]
+ if (t != null) {
+ if (t === '\n') {
+ col = begin.index! - lineIndex
+ slice = code.slice(begin.index, m.index!)
+ yield {
+ type,
+ text: slice,
+ line,
+ col,
+ right: col + slice.length,
+ bottom: line,
+ index: begin.index!,
+ length: slice.length,
+ source
+ }
+ begin = m
+
+ ++line
+ lineIndex = ++m.index!
+ }
+ else if (t === open) depth++
+ else if (t === close) {
+ if (!--depth) {
+ col = begin.index! - lineIndex
+ slice = code.slice(begin.index, m.index! + 1)
+ yield {
+ type,
+ text: slice,
+ line,
+ col,
+ right: col + slice.length,
+ bottom: line,
+ index: begin.index!,
+ length: slice.length,
+ source
+ }
+ continue outer
+ }
+ }
+ break
+ }
+ }
+ }
+ break
+ }
+ case Token.Type.Comment:
+ commenting = true
+ default:
+ col = match.index! - lineIndex
+ yield {
+ type: commenting ? Token.Type.Comment : type,
+ text,
+ line,
+ col,
+ right: col + text.length,
+ bottom: line,
+ index: match.index!,
+ length: text.length,
+ source
+ }
+ }
+ break
+ }
+ }
+ }
+}
diff --git a/src/pages/App.tsx b/src/pages/App.tsx
index cd456e7..a5d0635 100644
--- a/src/pages/App.tsx
+++ b/src/pages/App.tsx
@@ -8,12 +8,14 @@ import { Toast } from '~/src/comp/Toast.tsx'
import { VerifyEmail } from '~/src/comp/VerifyEmail.tsx'
import { About } from '~/src/pages/About.tsx'
import { AssemblyScript } from '~/src/pages/AssemblyScript.tsx'
-import { Canvas } from '~/src/pages/Canvas'
+import { CanvasDemo } from '~/src/pages/CanvasDemo'
import { Chat } from '~/src/pages/Chat/Chat.tsx'
+import { EditorDemo } from '~/src/pages/EditorDemo.tsx'
import { Home } from '~/src/pages/Home.tsx'
import { OAuthRegister } from '~/src/pages/OAuthRegister.tsx'
import { QrCode } from '~/src/pages/QrCode.tsx'
import { UiShowcase } from '~/src/pages/UiShowcase.tsx'
+import { WebGLDemo } from '~/src/pages/WebGLDemo.tsx'
import { WebSockets } from '~/src/pages/WebSockets.tsx'
import { whoami } from '~/src/rpc/auth.ts'
import { state } from '~/src/state.ts'
@@ -26,8 +28,8 @@ export function App() {
const info = $({
bg: 'transparent',
- canvasWidth: 500,
- canvasHeight: 500,
+ canvasWidth: state.$.containerWidth,
+ canvasHeight: 800,
})
const router = CachingRouter({
@@ -35,7 +37,9 @@ export function App() {
'/ui': () => ,
'/chat': () => ,
'!/ws': () => ,
- '!/canvas': () => ,
+ '!/canvas': () => ,
+ '/webgl': () => ,
+ '/editor': () => ,
'/asc': () => ,
'/qrcode': () => ,
'/about': () => ,
@@ -75,7 +79,7 @@ export function App() {
])
return info.bg = '#433'}
onmouseleave={() => info.bg = 'transparent'}
>
@@ -93,7 +97,7 @@ export function App() {