@alstar/studio 0.0.0-beta.1 → 0.0.0-beta.11

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 (69) hide show
  1. package/api/api-key.ts +69 -0
  2. package/api/auth.ts +66 -0
  3. package/api/backup.ts +40 -0
  4. package/api/block.ts +131 -66
  5. package/api/index.ts +19 -1
  6. package/api/mcp.ts +50 -0
  7. package/components/AdminPanel.ts +87 -0
  8. package/components/Backup.ts +13 -0
  9. package/components/BlockFieldRenderer.ts +125 -0
  10. package/components/BlockRenderer.ts +22 -0
  11. package/components/Entries.ts +20 -12
  12. package/components/Entry.ts +13 -21
  13. package/components/FieldRenderer.ts +35 -0
  14. package/components/Render.ts +46 -0
  15. package/components/Settings.ts +104 -0
  16. package/components/SiteLayout.ts +61 -0
  17. package/components/Users.ts +46 -0
  18. package/components/fields/Markdown.ts +44 -0
  19. package/components/fields/Slug.ts +113 -0
  20. package/components/fields/Text.ts +42 -0
  21. package/components/fields/index.ts +7 -0
  22. package/components/icons.ts +136 -7
  23. package/index.ts +94 -34
  24. package/package.json +10 -7
  25. package/pages/entry/[id].ts +15 -0
  26. package/pages/error.ts +14 -0
  27. package/{components → pages}/index.ts +7 -4
  28. package/pages/login.ts +21 -0
  29. package/pages/register.ts +33 -0
  30. package/pages/settings.ts +8 -0
  31. package/public/studio/css/admin-panel.css +103 -0
  32. package/public/studio/css/blocks-field.css +53 -0
  33. package/public/studio/css/settings.css +28 -0
  34. package/public/studio/js/markdown-editor.js +34 -0
  35. package/public/studio/js/sortable-list.js +50 -0
  36. package/public/studio/main.css +166 -0
  37. package/public/studio/main.js +21 -0
  38. package/queries/block-2.ts +339 -0
  39. package/queries/block-with-children.ts +74 -0
  40. package/queries/block.ts +289 -0
  41. package/queries/db-types.ts +15 -0
  42. package/queries/getBlockTrees-2.ts +71 -0
  43. package/queries/getBlockTrees.ts +316 -0
  44. package/queries/getBlocks.ts +214 -0
  45. package/queries/index.ts +2 -98
  46. package/queries/structure-types.ts +97 -0
  47. package/readme.md +205 -0
  48. package/schema.sql +18 -0
  49. package/schemas.ts +23 -52
  50. package/types.ts +144 -5
  51. package/utils/auth.ts +54 -0
  52. package/utils/buildBlocksTree.ts +4 -4
  53. package/utils/create-hash.ts +9 -0
  54. package/utils/define.ts +39 -0
  55. package/utils/file-based-router.ts +11 -2
  56. package/utils/get-config.ts +8 -9
  57. package/utils/get-or-create-row.ts +41 -0
  58. package/utils/html.ts +247 -0
  59. package/utils/startup-log.ts +19 -0
  60. package/components/AdminPanel/AdminPanel.css +0 -59
  61. package/components/AdminPanel/AdminPanel.ts +0 -57
  62. package/components/Block.ts +0 -116
  63. package/components/Entry.css +0 -7
  64. package/components/Field.ts +0 -164
  65. package/components/Fields.ts +0 -43
  66. package/components/layout.ts +0 -53
  67. package/public/main.css +0 -92
  68. package/public/main.js +0 -43
  69. /package/public/{favicon.svg → studio/favicon.svg} +0 -0
@@ -0,0 +1,39 @@
1
+ import * as types from '../types.ts'
2
+ import { type HtmlEscapedString } from './html.ts'
3
+
4
+ export const defineConfig = (config: types.StudioConfig) => config
5
+
6
+ export const defineEntry = (
7
+ fn: (
8
+ c: types.RequestContext,
9
+ ) => HtmlEscapedString | Promise<HtmlEscapedString>,
10
+ ) => fn
11
+
12
+ export const FieldInstance = Symbol('field')
13
+ export const BlockFieldInstance = Symbol('blockfield')
14
+ export const BlockInstance = Symbol('block')
15
+
16
+ // --- Identity helpers (preserve literal types) ---
17
+ export function defineField(field: types.FieldDef): types.FieldDefStructure {
18
+ return {
19
+ ...field,
20
+ instanceOf: FieldInstance,
21
+ }
22
+ }
23
+
24
+ export function defineBlockField(
25
+ field: types.BlocksFieldDef,
26
+ ): types.BlocksFieldDefStructure {
27
+ return {
28
+ ...field,
29
+ instanceOf: BlockFieldInstance,
30
+ }
31
+ }
32
+
33
+ export function defineBlock(block: types.BlockDef): types.BlockDefStructure {
34
+ return { ...block, instanceOf: BlockInstance }
35
+ }
36
+
37
+ export function defineStructure(structure: types.Structure) {
38
+ return structure
39
+ }
@@ -3,13 +3,22 @@ import path from 'node:path'
3
3
  import { Hono } from 'hono'
4
4
  import { type HtmlEscapedString } from 'hono/utils/html'
5
5
 
6
- type Page<C> = (c?: C) => HtmlEscapedString | Promise<HtmlEscapedString>
6
+ export type Page<C> = (c?: C) => HtmlEscapedString | Promise<HtmlEscapedString>
7
7
 
8
8
  export const fileBasedRouter = async (rootdir: string) => {
9
9
  const router = new Hono()
10
10
 
11
11
  const root = path.resolve(rootdir)
12
- const dirs = await fs.readdir(root, { recursive: true })
12
+
13
+ let dirs
14
+
15
+ try {
16
+ dirs = await fs.readdir(root, { recursive: true })
17
+ } catch (error) {
18
+ console.log('No files in:', root)
19
+ return
20
+ }
21
+
13
22
  const files = dirs.filter((dir) => path.extname(dir))
14
23
 
15
24
  await Promise.all(
@@ -3,15 +3,6 @@ import path from 'node:path'
3
3
 
4
4
  const CONFIG_FILE_NAME = 'alstar.config.ts'
5
5
 
6
- async function fileExists(filepath: string) {
7
- // does the file exist?
8
- try {
9
- await fs.stat(filepath)
10
- } catch (error) {
11
- return {}
12
- }
13
- }
14
-
15
6
  export const getConfig = async <P>(): Promise<P> => {
16
7
  const root = path.resolve('./')
17
8
 
@@ -24,3 +15,11 @@ export const getConfig = async <P>(): Promise<P> => {
24
15
 
25
16
  return config as P
26
17
  }
18
+
19
+ async function fileExists(filepath: string) {
20
+ try {
21
+ return await fs.stat(filepath)
22
+ } catch (error) {
23
+ return null
24
+ }
25
+ }
@@ -0,0 +1,41 @@
1
+ import { db } from '@alstar/db'
2
+ import { query } from '../queries/index.ts'
3
+ import type {
4
+ BlockDefStructure,
5
+ BlocksFieldDefStructure,
6
+ FieldDefStructure,
7
+ } from '../types.ts'
8
+
9
+ export function getOrCreateRow(props: {
10
+ parentId: string | number | null
11
+ name: string
12
+ field: BlockDefStructure | BlocksFieldDefStructure | FieldDefStructure
13
+ sortOrder?: number
14
+ id?: number
15
+ }) {
16
+ const { parentId, name, field, sortOrder = 0, id } = props
17
+
18
+ if (id) {
19
+ return query.block({
20
+ id: id?.toString(),
21
+ })
22
+ }
23
+
24
+ const data = query.block({
25
+ parent_id: parentId?.toString() || null,
26
+ name: name,
27
+ sort_order: sortOrder.toString(),
28
+ })
29
+
30
+ if (data) return data
31
+
32
+ const change = db.insertInto('blocks', {
33
+ name: name?.toString(),
34
+ label: field.label?.toString(),
35
+ type: field.type?.toString(),
36
+ sort_order: sortOrder,
37
+ parent_id: parentId,
38
+ })
39
+
40
+ return query.block({ id: change.lastInsertRowid.toString() })
41
+ }
package/utils/html.ts ADDED
@@ -0,0 +1,247 @@
1
+ export const HtmlEscapedCallbackPhase = {
2
+ Stringify: 1,
3
+ BeforeStream: 2,
4
+ Stream: 3,
5
+ } as const
6
+ type HtmlEscapedCallbackOpts = {
7
+ buffer?: [string]
8
+ phase: (typeof HtmlEscapedCallbackPhase)[keyof typeof HtmlEscapedCallbackPhase]
9
+ context: Readonly<object> // An object unique to each JSX tree. This object is used as the WeakMap key.
10
+ }
11
+ export type HtmlEscapedCallback = (
12
+ opts: HtmlEscapedCallbackOpts,
13
+ ) => Promise<string> | undefined
14
+ export type HtmlEscaped = {
15
+ isEscaped: true
16
+ callbacks?: HtmlEscapedCallback[]
17
+ }
18
+ export type HtmlEscapedString = string & HtmlEscaped
19
+
20
+ /**
21
+ * StringBuffer contains string and Promise<string> alternately
22
+ * The length of the array will be odd, the odd numbered element will be a string,
23
+ * and the even numbered element will be a Promise<string>.
24
+ * When concatenating into a single string, it must be processed from the tail.
25
+ * @example
26
+ * [
27
+ * 'framework.',
28
+ * Promise.resolve('ultra fast'),
29
+ * 'a ',
30
+ * Promise.resolve('is '),
31
+ * 'Hono',
32
+ * ]
33
+ */
34
+ export type StringBuffer = (string | Promise<string>)[]
35
+ export type StringBufferWithCallbacks = StringBuffer & {
36
+ callbacks: HtmlEscapedCallback[]
37
+ }
38
+
39
+ export const raw = (
40
+ value: unknown,
41
+ callbacks?: HtmlEscapedCallback[],
42
+ ): HtmlEscapedString => {
43
+ const escapedString = new String(value) as HtmlEscapedString
44
+ escapedString.isEscaped = true
45
+ escapedString.callbacks = callbacks
46
+
47
+ return escapedString
48
+ }
49
+
50
+ // The `escapeToBuffer` implementation is based on code from the MIT licensed `react-dom` package.
51
+ // https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/server/escapeTextForBrowser.js
52
+
53
+ const escapeRe = /[&<>'"]/
54
+
55
+ export const stringBufferToString = async (
56
+ buffer: StringBuffer,
57
+ callbacks: HtmlEscapedCallback[] | undefined,
58
+ ): Promise<HtmlEscapedString> => {
59
+ let str = ''
60
+ callbacks ||= []
61
+ const resolvedBuffer = await Promise.all(buffer)
62
+ for (let i = resolvedBuffer.length - 1; ; i--) {
63
+ str += resolvedBuffer[i]
64
+ i--
65
+ if (i < 0) {
66
+ break
67
+ }
68
+
69
+ let r = resolvedBuffer[i]
70
+ if (typeof r === 'object') {
71
+ callbacks.push(...((r as HtmlEscapedString).callbacks || []))
72
+ }
73
+
74
+ const isEscaped = (r as HtmlEscapedString).isEscaped
75
+ r = await (typeof r === 'object' ? (r as HtmlEscapedString).toString() : r)
76
+ if (typeof r === 'object') {
77
+ callbacks.push(...((r as HtmlEscapedString).callbacks || []))
78
+ }
79
+
80
+ if ((r as HtmlEscapedString).isEscaped ?? isEscaped) {
81
+ str += r
82
+ } else {
83
+ const buf = [str]
84
+ escapeToBuffer(r, buf)
85
+ str = buf[0]
86
+ }
87
+ }
88
+
89
+ return raw(str, callbacks)
90
+ }
91
+
92
+ export const escapeToBuffer = (str: string, buffer: StringBuffer): void => {
93
+ const match = str.search(escapeRe)
94
+ if (match === -1) {
95
+ buffer[0] += str
96
+ return
97
+ }
98
+
99
+ let escape
100
+ let index
101
+ let lastIndex = 0
102
+
103
+ for (index = match; index < str.length; index++) {
104
+ switch (str.charCodeAt(index)) {
105
+ case 34: // "
106
+ escape = '&quot;'
107
+ break
108
+ case 39: // '
109
+ escape = '&#39;'
110
+ break
111
+ case 38: // &
112
+ escape = '&amp;'
113
+ break
114
+ case 60: // <
115
+ escape = '&lt;'
116
+ break
117
+ case 62: // >
118
+ escape = '&gt;'
119
+ break
120
+ default:
121
+ continue
122
+ }
123
+
124
+ buffer[0] += str.substring(lastIndex, index) + escape
125
+ lastIndex = index + 1
126
+ }
127
+
128
+ buffer[0] += str.substring(lastIndex, index)
129
+ }
130
+
131
+ export const resolveCallbackSync = (
132
+ str: string | HtmlEscapedString,
133
+ ): string => {
134
+ const callbacks = (str as HtmlEscapedString)
135
+ .callbacks as HtmlEscapedCallback[]
136
+ if (!callbacks?.length) {
137
+ return str
138
+ }
139
+ const buffer: [string] = [str]
140
+ const context = {}
141
+
142
+ callbacks.forEach((c) =>
143
+ c({ phase: HtmlEscapedCallbackPhase.Stringify, buffer, context }),
144
+ )
145
+
146
+ return buffer[0]
147
+ }
148
+
149
+ export const resolveCallback = async (
150
+ str: string | HtmlEscapedString | Promise<string>,
151
+ phase: (typeof HtmlEscapedCallbackPhase)[keyof typeof HtmlEscapedCallbackPhase],
152
+ preserveCallbacks: boolean,
153
+ context: object,
154
+ buffer?: [string],
155
+ ): Promise<string> => {
156
+ if (typeof str === 'object' && !(str instanceof String)) {
157
+ if (!((str as unknown) instanceof Promise)) {
158
+ str = (str as unknown as string).toString() // HtmlEscapedString object to string
159
+ }
160
+ if ((str as string | Promise<string>) instanceof Promise) {
161
+ str = await (str as unknown as Promise<string>)
162
+ }
163
+ }
164
+
165
+ const callbacks = (str as HtmlEscapedString)
166
+ .callbacks as HtmlEscapedCallback[]
167
+ if (!callbacks?.length) {
168
+ return Promise.resolve(str)
169
+ }
170
+ if (buffer) {
171
+ buffer[0] += str
172
+ } else {
173
+ buffer = [str as string]
174
+ }
175
+
176
+ const resStr = Promise.all(
177
+ callbacks.map((c) => c({ phase, buffer, context })),
178
+ ).then((res) =>
179
+ Promise.all(
180
+ res
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ .filter<string>(Boolean as any)
183
+ .map((str) => resolveCallback(str, phase, false, context, buffer)),
184
+ ).then(() => (buffer as [string])[0]),
185
+ )
186
+
187
+ if (preserveCallbacks) {
188
+ return raw(await resStr, callbacks)
189
+ } else {
190
+ return resStr
191
+ }
192
+ }
193
+
194
+ export const html = (
195
+ strings: TemplateStringsArray,
196
+ ...values: unknown[]
197
+ ): HtmlEscapedString | Promise<HtmlEscapedString> => {
198
+ const buffer: StringBufferWithCallbacks = [''] as StringBufferWithCallbacks
199
+
200
+ for (let i = 0, len = strings.length - 1; i < len; i++) {
201
+ buffer[0] += strings[i]
202
+
203
+ const children = Array.isArray(values[i])
204
+ ? (values[i] as Array<unknown>).flat(Infinity)
205
+ : [values[i]]
206
+ for (let i = 0, len = children.length; i < len; i++) {
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
+ const child = children[i] as any
209
+ if (typeof child === 'string') {
210
+ escapeToBuffer(child, buffer)
211
+ } else if (typeof child === 'number') {
212
+ ;(buffer[0] as string) += child
213
+ } else if (
214
+ typeof child === 'boolean' ||
215
+ child === null ||
216
+ child === undefined
217
+ ) {
218
+ continue
219
+ } else if (
220
+ typeof child === 'object' &&
221
+ (child as HtmlEscaped).isEscaped
222
+ ) {
223
+ if ((child as HtmlEscapedString).callbacks) {
224
+ buffer.unshift('', child)
225
+ } else {
226
+ const tmp = child.toString()
227
+ if (tmp instanceof Promise) {
228
+ buffer.unshift('', tmp)
229
+ } else {
230
+ buffer[0] += tmp
231
+ }
232
+ }
233
+ } else if (child instanceof Promise) {
234
+ buffer.unshift('', child)
235
+ } else {
236
+ escapeToBuffer(child.toString(), buffer)
237
+ }
238
+ }
239
+ }
240
+ buffer[0] += strings.at(-1) as string
241
+
242
+ return buffer.length === 1
243
+ ? 'callbacks' in buffer
244
+ ? raw(resolveCallbackSync(raw(buffer[0], buffer.callbacks)))
245
+ : raw(buffer[0])
246
+ : stringBufferToString(buffer, buffer.callbacks)
247
+ }
@@ -0,0 +1,19 @@
1
+ import packageJSON from '../package.json' with { type: 'json' }
2
+
3
+ export default ({ port, refresherPort }: { port: number, refresherPort: number }) => {
4
+ console.log('\x1b[32m%s\x1b[0m', '╭───────────────╮')
5
+ console.log('\x1b[32m%s\x1b[0m', '│ Alstar Studio │')
6
+ console.log('\x1b[32m%s\x1b[0m', '╰───────────────╯')
7
+ console.log(' ')
8
+ console.log('\x1b[32m%s\x1b[0m', `Version:`)
9
+ console.log(packageJSON.version)
10
+ console.log(' ')
11
+ console.log('\x1b[32m%s\x1b[0m', `App:`)
12
+ console.log(`http://localhost:${port}`)
13
+ console.log(' ')
14
+ console.log('\x1b[32m%s\x1b[0m', `Studio:`)
15
+ console.log(`http://localhost:${port}/studio`)
16
+ console.log(' ')
17
+ console.log('\x1b[32m%s\x1b[0m', `Refresher:`)
18
+ console.log(`http://localhost:${refresherPort}`)
19
+ }
@@ -1,59 +0,0 @@
1
- .admin-panel {
2
- /* background: hsla(0, 0%, 0%, 0.1); */
3
- padding: 40px;
4
- padding-right: 0;
5
-
6
- height: 100%;
7
- min-height: inherit;
8
-
9
- min-width: 200px;
10
-
11
- > h1 {
12
- padding-bottom: 1rem;
13
-
14
- a {
15
- display: flex;
16
- }
17
-
18
- svg {
19
- height: 1.6rem;
20
- }
21
- }
22
-
23
- form {
24
- padding-bottom: 1rem;
25
-
26
- button {
27
- margin: 10px 0px 20px;
28
- }
29
- }
30
-
31
- #entries ul {
32
- padding: 0;
33
-
34
- > li {
35
- margin-bottom: 0px;
36
- border-radius: 8px;
37
- display: flex;
38
- justify-content: space-between;
39
- align-items: stretch;
40
- /* align-items: center; */
41
- list-style: none;
42
- margin-inline: -1rem;
43
-
44
- a {
45
- text-decoration: none;
46
- width: 100%;
47
- padding: 0.5rem 1rem;
48
- }
49
-
50
- button {
51
- border-radius: 7px;
52
-
53
- svg {
54
- margin: 0.5rem 1rem;
55
- }
56
- }
57
- }
58
- }
59
- }
@@ -1,57 +0,0 @@
1
- import { html } from 'hono/html'
2
- import type { Structure } from '../../types.ts'
3
- import { logo } from '../icons.ts'
4
- import { rootdir } from '../../index.ts'
5
- import Entries from '../Entries.ts'
6
- import * as icons from '../icons.ts'
7
-
8
- export default (structure: Structure) => {
9
- return html`
10
- <link
11
- rel="stylesheet"
12
- href="${rootdir}/components/AdminPanel/AdminPanel.css"
13
- />
14
-
15
- <div class="admin-panel">
16
- <h1>
17
- <a href="/admin" aria-label="Go to dashboard"> ${logo} </a>
18
- </h1>
19
-
20
- <aside>
21
- <form
22
- data-on-submit="@post('/admin/api/block', { contentType: 'form' })"
23
- >
24
- <!-- <select name="type">
25
- ${structure.map(
26
- (block) => html`
27
- <option value="${block.type}">
28
- ${block.label}
29
- </option>
30
- `,
31
- )}
32
- </select>
33
- <input type="submit" value="Create" /> -->
34
-
35
- <!-- TODO: currently only handles a single entry type -->
36
- ${structure.length
37
- ? html`<input
38
- type="hidden"
39
- name="type"
40
- value="${structure[0].type}"
41
- />
42
- <button
43
- class="ghost"
44
- style="padding: 10px; margin: 0 -13px; display: flex;"
45
- data-tooltip="New entry"
46
- data-placement="right"
47
- >
48
- ${icons.newDocument}
49
- </button>`
50
- : ''}
51
- </form>
52
- </aside>
53
-
54
- ${Entries()}
55
- </div>
56
- `
57
- }
@@ -1,116 +0,0 @@
1
- import { html } from 'hono/html'
2
- import { query } from '../queries/index.ts'
3
- import Fields from './Fields.ts'
4
- import * as icons from './icons.ts'
5
- import { type Block } from '../types.ts'
6
- import Field from './Field.ts'
7
- import {
8
- buildStructurePath,
9
- type StructurePath,
10
- } from '../utils/build-structure-path.ts'
11
-
12
- export default (
13
- entryId: string | number,
14
- id: string | number,
15
- fieldStructure: Block,
16
- structurePath: StructurePath,
17
- ) => {
18
- const data = query.block({ id: id.toString() })
19
- const blocks = query.blocks({ parent: id })
20
-
21
- if (!data) return html`<p>No block data</p>`
22
-
23
- const fieldStructurePath = buildStructurePath(fieldStructure, structurePath)
24
-
25
- return html`
26
-
27
- <section class="block">
28
- <header>
29
- <h5>${fieldStructure.label}</h5>
30
-
31
- <form
32
- data-on-submit="@post('/admin/api/new-block', { contentType: 'form' })"
33
- >
34
- <input type="hidden" name="parent_block_id" value="${id}" />
35
- <input
36
- type="hidden"
37
- name="sort_order"
38
- value="${blocks?.length || 0}"
39
- />
40
-
41
- <fieldset role="group">
42
- <select name="block">
43
- ${fieldStructure.fields?.map((field) => {
44
- return html`<option
45
- value="entry_id:${entryId};type:${field.type};name:${field.name};label:${field.label};sort_order:${blocks?.length ||
46
- 0}"
47
- >
48
- ${field.label}
49
- </option>`
50
- })}
51
- </select>
52
-
53
- <button
54
- class="outline"
55
- style="padding: 0 1rem"
56
- data-tooltip="New entry"
57
- data-placement="right"
58
- type="submit"
59
- >
60
- Add
61
- </button>
62
- </fieldset>
63
- </form>
64
- </header>
65
-
66
- <div data-sortable="${id}">
67
- ${blocks?.map((block, idx) => {
68
- const structure = fieldStructure.fields?.find(
69
- (field) => field.name === block.name,
70
- )
71
-
72
- if (!structure) return
73
-
74
- return html`
75
- <article
76
- data-signals="{ open${block.id}: true }"
77
- >
78
- <header>
79
- <h6>${structure.label}</h6>
80
- <div>
81
- <button
82
- data-on-click="$open${block.id} = !$open${block.id}"
83
- style="margin-right: 0.5rem"
84
- data-style-rotate="$open${block.id} ? '0deg' : '180deg'"
85
- class="shadow"
86
- >
87
- ${icons.chevron}
88
- </button>
89
- <button class="shadow">${icons.bars}</button>
90
- </div>
91
- </header>
92
-
93
- <div data-show="$open${block.id}">
94
- ${structure.fields
95
- ? Fields({
96
- entryId,
97
- parentId: block.id,
98
- blockStructure: structure,
99
- structurePath: [...fieldStructurePath, structure.name],
100
- })
101
- : Field({
102
- entryId,
103
- parentId: block.id,
104
- blockStructure: structure,
105
- sortOrder: idx,
106
- structurePath: [...fieldStructurePath, structure.name],
107
- })}
108
-
109
- </div>
110
- </article>
111
- `
112
- })}
113
- </div>
114
- </section>
115
- `
116
- }
@@ -1,7 +0,0 @@
1
- .entry {
2
- form {
3
- display: flex;
4
- flex-direction: column;
5
- align-items: flex-start;
6
- }
7
- }