@polytric/openws-sdkgen 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +2 -2
  2. package/dist/main.cjs +53 -5
  3. package/dist/main.js +53 -5
  4. package/dist/plans/dotnet.cjs +3 -3
  5. package/dist/plans/dotnet.d.cts +2 -143
  6. package/dist/plans/dotnet.d.ts +2 -143
  7. package/dist/plans/dotnet.js +2 -2
  8. package/dist/plans/typescript.cjs +569 -0
  9. package/dist/plans/typescript.d.cts +6 -0
  10. package/dist/plans/typescript.d.ts +6 -0
  11. package/dist/plans/typescript.js +532 -0
  12. package/dist/templates/dotnet/HostRole.cs.ejs +23 -8
  13. package/dist/templates/dotnet/Model.cs.ejs +1 -1
  14. package/dist/templates/dotnet/RemoteRole.cs.ejs +7 -2
  15. package/dist/templates/dotnet/UserHostRole.cs.ejs +26 -4
  16. package/dist/templates/typescript/package.json.ejs +41 -0
  17. package/dist/templates/typescript/src/core/index.ts.ejs +6 -0
  18. package/dist/templates/typescript/src/core/models/index.ts.ejs +3 -0
  19. package/dist/templates/typescript/src/core/models/model.ts.ejs +41 -0
  20. package/dist/templates/typescript/src/core/network.ts.ejs +517 -0
  21. package/dist/templates/typescript/src/core/roles/index.ts.ejs +3 -0
  22. package/dist/templates/typescript/src/core/roles/role.ts.ejs +104 -0
  23. package/dist/templates/typescript/src/index.ts.ejs +4 -0
  24. package/dist/templates/typescript/src/sdk/index.ts.ejs +3 -0
  25. package/dist/templates/typescript/src/sdk/role.ts.ejs +372 -0
  26. package/dist/templates/typescript/tsconfig.json.ejs +14 -0
  27. package/dist/templates/typescript/tsup.config.ts.ejs +10 -0
  28. package/dist/types-BdZPs123.d.cts +115 -0
  29. package/dist/types-BdZPs123.d.ts +115 -0
  30. package/package.json +13 -4
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": <%- JSON.stringify(ctx.packageName) %>,
3
+ "version": <%- JSON.stringify(ctx.version) %>,
4
+ "description": <%- JSON.stringify(ctx.description ?? '') %>,
5
+ "type": "module",
6
+ <% if (ctx.isTypeScript) { -%>
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@polytric/openws": "^0.0.4"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "devDependencies": {
26
+ "tsup": "^8.5.1",
27
+ "typescript": "^5.9.3"
28
+ }
29
+ <% } else { -%>
30
+ "main": "./src/index.js",
31
+ "exports": {
32
+ ".": "./src/index.js"
33
+ },
34
+ "files": [
35
+ "src"
36
+ ],
37
+ "dependencies": {
38
+ "@polytric/openws": "^0.0.4"
39
+ }
40
+ <% } -%>
41
+ }
@@ -0,0 +1,6 @@
1
+ export * from './network<%= ctx.isTypeScript ? '' : '.js' %>'
2
+ export * from './roles<%= ctx.isTypeScript ? '' : '/index.js' %>'
3
+ export * as roles from './roles<%= ctx.isTypeScript ? '' : '/index.js' %>'
4
+ <% for (const modelScope of ctx.modelScopes) { -%>
5
+ export * as <%= modelScope.varName %>Models from './models/<%= modelScope.fileName %><%= ctx.isTypeScript ? '' : '/index.js' %>'
6
+ <% } -%>
@@ -0,0 +1,3 @@
1
+ <% for (const model of ctx.models) { -%>
2
+ export * from './<%= model.fileName %><%= ctx.isTypeScript ? '' : '.js' %>'
3
+ <% } -%>
@@ -0,0 +1,41 @@
1
+ <% if (ctx.isTypeScript) { -%>
2
+ <% for (const modelImport of ctx.imports) { -%>
3
+ import type { <%= modelImport.className %> } from './<%= modelImport.fileName %>'
4
+ <% } -%>
5
+ <% if (ctx.imports.length > 0) { -%>
6
+
7
+ <% } -%>
8
+ export interface <%= ctx.className %>Init {
9
+ <% for (const property of ctx.properties) { -%>
10
+ <%= property.name %><%= property.optional ? '?' : '' %>: <%= property.typeName %>
11
+ <% } -%>
12
+ }
13
+
14
+ export class <%= ctx.className %> implements <%= ctx.className %>Init {
15
+ <% for (const property of ctx.properties) { -%>
16
+ readonly <%= property.name %><%= property.optional ? '?' : '' %>: <%= property.typeName %>
17
+ <% } -%>
18
+
19
+ constructor({
20
+ <% for (const property of ctx.properties) { -%>
21
+ <%= property.name %>,
22
+ <% } -%>
23
+ }: <%= ctx.className %>Init = {} as <%= ctx.className %>Init) {
24
+ <% for (const property of ctx.properties) { -%>
25
+ this.<%= property.name %> = <%= property.name %>
26
+ <% } -%>
27
+ }
28
+ }
29
+ <% } else { -%>
30
+ export class <%= ctx.className %> {
31
+ constructor({
32
+ <% for (const property of ctx.properties) { -%>
33
+ <%= property.name %>,
34
+ <% } -%>
35
+ } = {}) {
36
+ <% for (const property of ctx.properties) { -%>
37
+ this.<%= property.name %> = <%= property.name %>
38
+ <% } -%>
39
+ }
40
+ }
41
+ <% } -%>
@@ -0,0 +1,517 @@
1
+ export const networkName = <%- JSON.stringify(ctx.networkName) %>
2
+ export const networkDescription = <%- JSON.stringify(ctx.description ?? '') %>
3
+ export const networkVersion = <%- JSON.stringify(ctx.version ?? '1.0.0') %>
4
+
5
+ export const endpoints = <%- JSON.stringify(
6
+ Object.fromEntries(ctx.allRoles.map(role => [role.roleName, role.endpoints])),
7
+ null,
8
+ 4
9
+ ) %>
10
+
11
+ <% if (ctx.isTypeScript) { -%>
12
+ export interface OpenWsEnvelope<Payload = unknown> {
13
+ fromRole: string
14
+ messageName: string
15
+ payload: Payload
16
+ }
17
+
18
+ export interface OpenWsEndpoint {
19
+ scheme: string
20
+ host?: string
21
+ port?: number
22
+ path?: string
23
+ }
24
+
25
+ export type Unsubscribe = () => void
26
+ export type TransportEvent = 'message' | 'error'
27
+ export type TransportHandler = (data: unknown) => void | Promise<void>
28
+
29
+ export interface Transport {
30
+ send(data: string): void | Promise<void>
31
+ on?(event: TransportEvent, handler: TransportHandler): unknown
32
+ close?(): void
33
+ connect?(roleName: string, endpoint?: OpenWsEndpoint): void | Promise<void>
34
+ disconnect?(roleName: string): void | Promise<void>
35
+ }
36
+
37
+ export interface BindTransportOptions {
38
+ closeOnError?: boolean
39
+ }
40
+
41
+ export interface RawMessageHandler {
42
+ handleRawMessage(data: string): void | Promise<void>
43
+ messageError?(error: unknown): void | Promise<void>
44
+ socketError?(error: unknown): void | Promise<void>
45
+ }
46
+
47
+ export function encodeEnvelope(envelope: OpenWsEnvelope): string {
48
+ return JSON.stringify(envelope)
49
+ }
50
+
51
+ export function decodeEnvelope(data: string): OpenWsEnvelope {
52
+ return JSON.parse(data) as OpenWsEnvelope
53
+ }
54
+
55
+ export function canBindTransport(transport: Transport): boolean {
56
+ return typeof transport.on === 'function'
57
+ }
58
+
59
+ export function bindTransport(
60
+ transport: Transport,
61
+ handler: RawMessageHandler,
62
+ options: BindTransportOptions = {}
63
+ ): Unsubscribe {
64
+ const handleData = async (data: unknown) => {
65
+ try {
66
+ await handler.handleRawMessage(await normalizeMessageData(data))
67
+ } catch (error) {
68
+ await handler.messageError?.(error)
69
+ if (options.closeOnError) {
70
+ transport.close?.()
71
+ }
72
+ }
73
+ }
74
+ const handleSocketError = async (error: unknown) => {
75
+ await handler.socketError?.(error)
76
+ }
77
+
78
+ const nodeHandler = (data: unknown, ..._args: unknown[]) => {
79
+ void handleData(data)
80
+ }
81
+ const nodeErrorHandler = (error: unknown, ..._args: unknown[]) => {
82
+ void handleSocketError(error)
83
+ }
84
+
85
+ if (typeof transport.on !== 'function') {
86
+ throw new Error('Transport must support on("message")')
87
+ }
88
+
89
+ const messageUnsubscribe = transport.on('message', nodeHandler)
90
+ const errorUnsubscribe = transport.on('error', nodeErrorHandler)
91
+ return () => {
92
+ if (typeof messageUnsubscribe === 'function') messageUnsubscribe()
93
+ if (typeof errorUnsubscribe === 'function') errorUnsubscribe()
94
+ }
95
+ }
96
+
97
+ export class WsTransport implements Transport {
98
+ private socket?: unknown
99
+ private socketUnsubscribe?: Unsubscribe
100
+ private openPromise?: Promise<void>
101
+ private readonly listeners: Record<TransportEvent, Set<TransportHandler>> = {
102
+ message: new Set(),
103
+ error: new Set(),
104
+ }
105
+
106
+ constructor(socket?: unknown) {
107
+ if (socket) {
108
+ this.bindSocket(socket)
109
+ }
110
+ }
111
+
112
+ async connect(_roleName: string, endpoint?: OpenWsEndpoint): Promise<void> {
113
+ if (!this.socket) {
114
+ if (!endpoint) {
115
+ throw new Error('Cannot connect without a WebSocket endpoint')
116
+ }
117
+ this.bindSocket(createWebSocket(endpoint))
118
+ }
119
+ await this.waitForOpen()
120
+ }
121
+
122
+ async disconnect(): Promise<void> {
123
+ this.close()
124
+ }
125
+
126
+ async send(data: string): Promise<void> {
127
+ await this.waitForOpen()
128
+ const socket = this.requireSocket() as { send?: (data: string) => void | Promise<void> }
129
+ if (typeof socket.send !== 'function') {
130
+ throw new Error('WebSocket object must support send(data)')
131
+ }
132
+ await socket.send(data)
133
+ }
134
+
135
+ on(event: TransportEvent, handler: TransportHandler): Unsubscribe {
136
+ this.listeners[event].add(handler)
137
+ return () => {
138
+ this.listeners[event].delete(handler)
139
+ }
140
+ }
141
+
142
+ close(): void {
143
+ const socket = this.socket as { close?: () => void } | undefined
144
+ socket?.close?.()
145
+ this.socketUnsubscribe?.()
146
+ this.socket = undefined
147
+ this.socketUnsubscribe = undefined
148
+ this.openPromise = undefined
149
+ }
150
+
151
+ private bindSocket(socket: unknown): void {
152
+ this.socketUnsubscribe?.()
153
+ this.socket = socket
154
+ this.openPromise = undefined
155
+
156
+ const unsubscribeMessage = addSocketListener(socket, 'message', data => {
157
+ void this.emit('message', getMessageEventData(data))
158
+ })
159
+ const unsubscribeError = addSocketListener(socket, 'error', error => {
160
+ void this.emit('error', error)
161
+ })
162
+ this.socketUnsubscribe = () => {
163
+ unsubscribeMessage()
164
+ unsubscribeError()
165
+ }
166
+ }
167
+
168
+ private async emit(event: TransportEvent, data: unknown): Promise<void> {
169
+ for (const handler of this.listeners[event]) {
170
+ await handler(data)
171
+ }
172
+ }
173
+
174
+ private requireSocket(): unknown {
175
+ if (!this.socket) {
176
+ throw new Error('WebSocket is not connected')
177
+ }
178
+ return this.socket
179
+ }
180
+
181
+ private async waitForOpen(): Promise<void> {
182
+ const socket = this.requireSocket()
183
+ const readyState = getReadyState(socket)
184
+ if (readyState === undefined || readyState === 1) return
185
+ if (readyState === 2 || readyState === 3) {
186
+ throw new Error('WebSocket is closed')
187
+ }
188
+
189
+ this.openPromise ??= new Promise<void>((resolve, reject) => {
190
+ let cleanup: Unsubscribe = () => {}
191
+ const unsubscribeOpen = addSocketListener(socket, 'open', () => {
192
+ cleanup()
193
+ resolve()
194
+ })
195
+ const unsubscribeError = addSocketListener(socket, 'error', error => {
196
+ cleanup()
197
+ reject(error)
198
+ })
199
+ cleanup = () => {
200
+ unsubscribeOpen()
201
+ unsubscribeError()
202
+ }
203
+ })
204
+ await this.openPromise
205
+ }
206
+ }
207
+
208
+ async function normalizeMessageData(data: unknown): Promise<string> {
209
+ if (typeof data === 'string') return data
210
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data)
211
+ if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data)
212
+ if (typeof Blob !== 'undefined' && data instanceof Blob) return await data.text()
213
+ return String(data)
214
+ }
215
+
216
+ function getMessageEventData(event: unknown): unknown {
217
+ if (event && typeof event === 'object' && 'data' in event) {
218
+ return (event as { data: unknown }).data
219
+ }
220
+ return event
221
+ }
222
+
223
+ function createWebSocket(endpoint: OpenWsEndpoint): unknown {
224
+ const WebSocketCtor = (globalThis as { WebSocket?: new (url: string) => unknown }).WebSocket
225
+ if (!WebSocketCtor) {
226
+ throw new Error('No global WebSocket constructor found. Pass a socket or custom Transport to the client.')
227
+ }
228
+ return new WebSocketCtor(endpointToUrl(endpoint))
229
+ }
230
+
231
+ function endpointToUrl(endpoint: OpenWsEndpoint): string {
232
+ const scheme = endpoint.scheme || 'ws'
233
+ const host = endpoint.host || 'localhost'
234
+ const port = endpoint.port === undefined ? '' : `:${endpoint.port}`
235
+ const path = endpoint.path ? (endpoint.path.startsWith('/') ? endpoint.path : `/${endpoint.path}`) : ''
236
+ return `${scheme}://${host}${port}${path}`
237
+ }
238
+
239
+ function getReadyState(socket: unknown): number | undefined {
240
+ const readyState = (socket as { readyState?: unknown }).readyState
241
+ return typeof readyState === 'number' ? readyState : undefined
242
+ }
243
+
244
+ function addSocketListener(
245
+ socket: unknown,
246
+ event: 'open' | 'message' | 'error',
247
+ handler: (...args: unknown[]) => void
248
+ ): Unsubscribe {
249
+ const target = socket as {
250
+ on?: (event: string, handler: (...args: unknown[]) => void) => unknown
251
+ off?: (event: string, handler: (...args: unknown[]) => void) => unknown
252
+ removeListener?: (event: string, handler: (...args: unknown[]) => void) => unknown
253
+ addEventListener?: (event: string, handler: (event: unknown) => void) => unknown
254
+ removeEventListener?: (event: string, handler: (event: unknown) => void) => unknown
255
+ [key: string]: unknown
256
+ }
257
+
258
+ if (typeof target.on === 'function') {
259
+ target.on(event, handler)
260
+ return () => {
261
+ if (typeof target.off === 'function') target.off(event, handler)
262
+ else if (typeof target.removeListener === 'function') target.removeListener(event, handler)
263
+ }
264
+ }
265
+
266
+ if (typeof target.addEventListener === 'function') {
267
+ const eventHandler = (event: unknown) => {
268
+ handler(event)
269
+ }
270
+ target.addEventListener(event, eventHandler)
271
+ return () => {
272
+ target.removeEventListener?.(event, eventHandler)
273
+ }
274
+ }
275
+
276
+ const propertyName = `on${event}`
277
+ const previous = target[propertyName]
278
+ const next = (...args: unknown[]) => {
279
+ if (typeof previous === 'function') {
280
+ previous(...args)
281
+ }
282
+ handler(...args)
283
+ }
284
+ target[propertyName] = next
285
+ return () => {
286
+ if (target[propertyName] === next) {
287
+ target[propertyName] = previous
288
+ }
289
+ }
290
+ }
291
+ <% } else { -%>
292
+ export function encodeEnvelope(envelope) {
293
+ return JSON.stringify(envelope)
294
+ }
295
+
296
+ export function decodeEnvelope(data) {
297
+ return JSON.parse(data)
298
+ }
299
+
300
+ export function canBindTransport(transport) {
301
+ return typeof transport.on === 'function'
302
+ }
303
+
304
+ export function bindTransport(transport, handler, options = {}) {
305
+ const handleData = async data => {
306
+ try {
307
+ await handler.handleRawMessage(await normalizeMessageData(data))
308
+ } catch (error) {
309
+ await handler.messageError?.(error)
310
+ if (options.closeOnError) {
311
+ transport.close?.()
312
+ }
313
+ }
314
+ }
315
+ const handleSocketError = async error => {
316
+ await handler.socketError?.(error)
317
+ }
318
+
319
+ const nodeHandler = data => {
320
+ void handleData(data)
321
+ }
322
+ const nodeErrorHandler = error => {
323
+ void handleSocketError(error)
324
+ }
325
+
326
+ if (typeof transport.on !== 'function') {
327
+ throw new Error('Transport must support on("message")')
328
+ }
329
+
330
+ const messageUnsubscribe = transport.on('message', nodeHandler)
331
+ const errorUnsubscribe = transport.on('error', nodeErrorHandler)
332
+ return () => {
333
+ if (typeof messageUnsubscribe === 'function') messageUnsubscribe()
334
+ if (typeof errorUnsubscribe === 'function') errorUnsubscribe()
335
+ }
336
+ }
337
+
338
+ export class WsTransport {
339
+ socket
340
+ socketUnsubscribe
341
+ openPromise
342
+ listeners = {
343
+ message: new Set(),
344
+ error: new Set(),
345
+ }
346
+
347
+ constructor(socket) {
348
+ if (socket) {
349
+ this.bindSocket(socket)
350
+ }
351
+ }
352
+
353
+ async connect(_roleName, endpoint) {
354
+ if (!this.socket) {
355
+ if (!endpoint) {
356
+ throw new Error('Cannot connect without a WebSocket endpoint')
357
+ }
358
+ this.bindSocket(createWebSocket(endpoint))
359
+ }
360
+ await this.waitForOpen()
361
+ }
362
+
363
+ async disconnect() {
364
+ this.close()
365
+ }
366
+
367
+ async send(data) {
368
+ await this.waitForOpen()
369
+ const socket = this.requireSocket()
370
+ if (typeof socket.send !== 'function') {
371
+ throw new Error('WebSocket object must support send(data)')
372
+ }
373
+ await socket.send(data)
374
+ }
375
+
376
+ on(event, handler) {
377
+ this.listeners[event].add(handler)
378
+ return () => {
379
+ this.listeners[event].delete(handler)
380
+ }
381
+ }
382
+
383
+ close() {
384
+ this.socket?.close?.()
385
+ this.socketUnsubscribe?.()
386
+ this.socket = undefined
387
+ this.socketUnsubscribe = undefined
388
+ this.openPromise = undefined
389
+ }
390
+
391
+ bindSocket(socket) {
392
+ this.socketUnsubscribe?.()
393
+ this.socket = socket
394
+ this.openPromise = undefined
395
+
396
+ const unsubscribeMessage = addSocketListener(socket, 'message', data => {
397
+ void this.emit('message', getMessageEventData(data))
398
+ })
399
+ const unsubscribeError = addSocketListener(socket, 'error', error => {
400
+ void this.emit('error', error)
401
+ })
402
+ this.socketUnsubscribe = () => {
403
+ unsubscribeMessage()
404
+ unsubscribeError()
405
+ }
406
+ }
407
+
408
+ async emit(event, data) {
409
+ for (const handler of this.listeners[event]) {
410
+ await handler(data)
411
+ }
412
+ }
413
+
414
+ requireSocket() {
415
+ if (!this.socket) {
416
+ throw new Error('WebSocket is not connected')
417
+ }
418
+ return this.socket
419
+ }
420
+
421
+ async waitForOpen() {
422
+ const socket = this.requireSocket()
423
+ const readyState = getReadyState(socket)
424
+ if (readyState === undefined || readyState === 1) return
425
+ if (readyState === 2 || readyState === 3) {
426
+ throw new Error('WebSocket is closed')
427
+ }
428
+
429
+ this.openPromise ??= new Promise((resolve, reject) => {
430
+ let cleanup = () => {}
431
+ const unsubscribeOpen = addSocketListener(socket, 'open', () => {
432
+ cleanup()
433
+ resolve()
434
+ })
435
+ const unsubscribeError = addSocketListener(socket, 'error', error => {
436
+ cleanup()
437
+ reject(error)
438
+ })
439
+ cleanup = () => {
440
+ unsubscribeOpen()
441
+ unsubscribeError()
442
+ }
443
+ })
444
+ await this.openPromise
445
+ }
446
+ }
447
+
448
+ async function normalizeMessageData(data) {
449
+ if (typeof data === 'string') return data
450
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data)
451
+ if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data)
452
+ if (typeof Blob !== 'undefined' && data instanceof Blob) return await data.text()
453
+ return String(data)
454
+ }
455
+
456
+ function getMessageEventData(event) {
457
+ if (event && typeof event === 'object' && 'data' in event) {
458
+ return event.data
459
+ }
460
+ return event
461
+ }
462
+
463
+ function createWebSocket(endpoint) {
464
+ const WebSocketCtor = globalThis.WebSocket
465
+ if (!WebSocketCtor) {
466
+ throw new Error('No global WebSocket constructor found. Pass a socket or custom Transport to the client.')
467
+ }
468
+ return new WebSocketCtor(endpointToUrl(endpoint))
469
+ }
470
+
471
+ function endpointToUrl(endpoint) {
472
+ const scheme = endpoint.scheme || 'ws'
473
+ const host = endpoint.host || 'localhost'
474
+ const port = endpoint.port === undefined ? '' : `:${endpoint.port}`
475
+ const path = endpoint.path ? (endpoint.path.startsWith('/') ? endpoint.path : `/${endpoint.path}`) : ''
476
+ return `${scheme}://${host}${port}${path}`
477
+ }
478
+
479
+ function getReadyState(socket) {
480
+ return typeof socket.readyState === 'number' ? socket.readyState : undefined
481
+ }
482
+
483
+ function addSocketListener(socket, event, handler) {
484
+ if (typeof socket.on === 'function') {
485
+ socket.on(event, handler)
486
+ return () => {
487
+ if (typeof socket.off === 'function') socket.off(event, handler)
488
+ else if (typeof socket.removeListener === 'function') socket.removeListener(event, handler)
489
+ }
490
+ }
491
+
492
+ if (typeof socket.addEventListener === 'function') {
493
+ const eventHandler = event => {
494
+ handler(event)
495
+ }
496
+ socket.addEventListener(event, eventHandler)
497
+ return () => {
498
+ socket.removeEventListener?.(event, eventHandler)
499
+ }
500
+ }
501
+
502
+ const propertyName = `on${event}`
503
+ const previous = socket[propertyName]
504
+ const next = (...args) => {
505
+ if (typeof previous === 'function') {
506
+ previous(...args)
507
+ }
508
+ handler(...args)
509
+ }
510
+ socket[propertyName] = next
511
+ return () => {
512
+ if (socket[propertyName] === next) {
513
+ socket[propertyName] = previous
514
+ }
515
+ }
516
+ }
517
+ <% } -%>
@@ -0,0 +1,3 @@
1
+ <% for (const role of ctx.roles) { -%>
2
+ export * from './<%= role.fileName %><%= ctx.isTypeScript ? '' : '.js' %>'
3
+ <% } -%>