@slidev/client 0.48.0-beta.14 → 0.48.0-beta.16

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.
package/builtin/Arrow.vue CHANGED
@@ -9,7 +9,7 @@ Simple Arrow
9
9
  -->
10
10
 
11
11
  <script setup lang="ts">
12
- import { customAlphabet } from 'nanoid'
12
+ import { makeId } from '../logic/utils'
13
13
 
14
14
  defineProps<{
15
15
  x1: number | string
@@ -20,9 +20,7 @@ defineProps<{
20
20
  color?: string
21
21
  }>()
22
22
 
23
- const nanoid = customAlphabet('abcedfghijklmn', 10)
24
-
25
- const id = nanoid()
23
+ const id = makeId()
26
24
  </script>
27
25
 
28
26
  <template>
@@ -19,7 +19,7 @@ import ShadowRoot from '../internals/ShadowRoot.vue'
19
19
  import { isDark } from '../logic/dark'
20
20
 
21
21
  const props = defineProps<{
22
- code: string
22
+ codeLz: string
23
23
  scale?: number
24
24
  theme?: string
25
25
  }>()
@@ -37,7 +37,7 @@ watchEffect(async (onCleanup) => {
37
37
  error.value = null
38
38
  try {
39
39
  const svg = await renderMermaid(
40
- props.code || '',
40
+ props.codeLz || '',
41
41
  {
42
42
  theme: props.theme || (isDark.value ? 'dark' : undefined),
43
43
  ...vm!.attrs,
@@ -12,122 +12,138 @@ Learn more: https://sli.dev/guide/syntax.html#monaco-editor
12
12
  -->
13
13
 
14
14
  <script setup lang="ts">
15
- import { computed, onMounted, ref } from 'vue'
16
- import { useEventListener } from '@vueuse/core'
17
- import { decode } from 'js-base64'
18
- import { nanoid } from 'nanoid'
19
- import type * as monaco from 'monaco-editor'
20
- import { isDark } from '../logic/dark'
15
+ import * as monaco from 'monaco-editor'
16
+ import { computed, nextTick, onMounted, ref } from 'vue'
17
+ import { debounce } from '@antfu/utils'
18
+ import { decompressFromBase64 } from 'lz-string'
19
+ import setup from '../setup/monaco'
20
+ import { makeId } from '../logic/utils'
21
21
 
22
22
  const props = withDefaults(defineProps<{
23
- code: string
24
- diff?: string
23
+ codeLz: string
24
+ diffLz?: string
25
25
  lang?: string
26
26
  readonly?: boolean
27
27
  lineNumbers?: 'on' | 'off' | 'relative' | 'interval'
28
- height?: number | string
28
+ height?: number | string // Posible values: 'initial', 'auto', '100%', '200px', etc.
29
29
  editorOptions?: monaco.editor.IEditorOptions
30
+ ata?: boolean
30
31
  }>(), {
31
- code: '',
32
+ codeLz: '',
32
33
  lang: 'typescript',
33
34
  readonly: false,
34
35
  lineNumbers: 'off',
35
- height: 'auto',
36
+ height: 'initial',
37
+ ata: true,
36
38
  })
37
39
 
38
- const id = nanoid()
39
- const code = ref(decode(props.code).trimEnd())
40
- const diff = ref(props.diff ? decode(props.diff).trimEnd() : null)
41
- const lineHeight = +(getComputedStyle(document.body).getPropertyValue('--slidev-code-line-height') || '18').replace('px', '') || 18
42
- const editorHeight = ref(0)
43
- const calculatedHeight = computed(() => code.value.split(/\r?\n/g).length * lineHeight)
44
- const height = computed(() => {
45
- return props.height === 'auto' ? `${Math.max(calculatedHeight.value, editorHeight.value) + 20}px` : props.height
46
- })
47
-
48
- const iframe = ref<HTMLIFrameElement>()
40
+ const code = decompressFromBase64(props.codeLz).trimEnd()
41
+ const diff = props.diffLz && decompressFromBase64(props.diffLz).trimEnd()
49
42
 
50
- const cssVars = [
51
- '--slidev-code-font-size',
52
- '--slidev-code-font-family',
53
- '--slidev-code-background',
54
- '--slidev-code-line-height',
55
- '--slidev-code-padding',
56
- '--slidev-code-margin',
57
- '--slidev-code-radius',
58
- ]
59
-
60
- function getStyleObject(el: Element) {
61
- const object: Record<string, string> = {}
62
- const style = getComputedStyle(el)
63
- for (const v of cssVars)
64
- object[v] = style.getPropertyValue(v)
65
- return object
43
+ const langMap: Record<string, string> = {
44
+ ts: 'typescript',
45
+ js: 'javascript',
66
46
  }
47
+ const lang = langMap[props.lang] ?? props.lang
48
+ const extMap: Record<string, string> = {
49
+ typescript: 'mts',
50
+ javascript: 'mjs',
51
+ ts: 'mts',
52
+ js: 'mjs',
53
+ }
54
+ const ext = extMap[props.lang] ?? props.lang
67
55
 
68
- onMounted(() => {
69
- const frame = iframe.value!
70
- frame.setAttribute('sandbox', [
71
- 'allow-forms',
72
- 'allow-modals',
73
- 'allow-pointer-lock',
74
- 'allow-popups',
75
- 'allow-same-origin',
76
- 'allow-scripts',
77
- 'allow-top-navigation-by-user-activation',
78
- ].join(' '))
79
-
80
- let src = __DEV__
81
- ? `${location.origin}${__SLIDEV_CLIENT_ROOT__}/`
82
- : import.meta.env.BASE_URL
83
- src += `iframes/monaco/index.html?id=${id}&lineNumbers=${props.lineNumbers}&lang=${props.lang}`
84
- if (diff.value)
85
- src += '&diff=1'
86
- frame.src = src
56
+ const outer = ref<HTMLDivElement>()
57
+ const container = ref<HTMLDivElement>()
87
58
 
88
- frame.style.backgroundColor = 'transparent'
59
+ const contentHeight = ref(0)
60
+ const initialHeight = ref<number>()
61
+ const height = computed(() => {
62
+ if (props.height === 'auto')
63
+ return `${contentHeight.value}px`
64
+ if (props.height === 'initial')
65
+ return `${initialHeight.value}px`
66
+ return props.height
89
67
  })
90
68
 
91
- function post(payload: any) {
92
- iframe.value?.contentWindow?.postMessage(
93
- JSON.stringify({
94
- type: 'slidev-monaco',
95
- data: payload,
96
- id,
97
- }),
98
- location.origin,
99
- )
100
- }
69
+ onMounted(async () => {
70
+ const { ata } = await setup()
71
+ const model = monaco.editor.createModel(code, lang, monaco.Uri.parse(`file:///${makeId()}.${ext}`))
72
+ const commonOptions = {
73
+ automaticLayout: true,
74
+ readOnly: props.readonly,
75
+ lineNumbers: props.lineNumbers,
76
+ minimap: { enabled: false },
77
+ overviewRulerBorder: false,
78
+ overviewRulerLanes: 0,
79
+ padding: { top: 10, bottom: 10 },
80
+ lineNumbersMinChars: 3,
81
+ bracketPairColorization: { enabled: false },
82
+ tabSize: 2,
83
+ fontSize: 11.5,
84
+ fontFamily: 'var(--slidev-code-font-family)',
85
+ scrollBeyondLastLine: false,
86
+ ...props.editorOptions,
87
+ } satisfies monaco.editor.IStandaloneEditorConstructionOptions & monaco.editor.IDiffEditorConstructionOptions
101
88
 
102
- useEventListener(window, 'message', ({ data: payload }) => {
103
- if (payload.id !== id)
104
- return
105
- if (payload.type === 'slidev-monaco-loaded') {
106
- if (iframe.value) {
107
- post({
108
- code: code.value,
109
- diff: diff.value,
110
- lang: props.lang,
111
- readonly: props.readonly,
112
- lineNumbers: props.lineNumbers,
113
- editorOptions: props.editorOptions,
114
- dark: isDark.value,
115
- style: Object.entries(getStyleObject(iframe.value)).map(([k, v]) => `${k}: ${v};`).join(''),
116
- })
89
+ let editableEditor: monaco.editor.IStandaloneCodeEditor
90
+ if (diff) {
91
+ const diffModel = monaco.editor.createModel(diff, lang, monaco.Uri.parse(`file:///${nanoid()}.${ext}`))
92
+ const editor = monaco.editor.createDiffEditor(container.value!, {
93
+ renderOverviewRuler: false,
94
+ ...commonOptions,
95
+ })
96
+ editor.setModel({
97
+ original: model,
98
+ modified: diffModel,
99
+ })
100
+ const originalEditor = editor.getOriginalEditor()
101
+ const modifiedEditor = editor.getModifiedEditor()
102
+ const onContentSizeChange = () => {
103
+ const newHeight = Math.max(originalEditor.getContentHeight(), modifiedEditor.getContentHeight()) + 4
104
+ initialHeight.value ??= newHeight
105
+ contentHeight.value = newHeight
106
+ nextTick(() => editor.layout())
107
+ }
108
+ originalEditor.onDidContentSizeChange(onContentSizeChange)
109
+ modifiedEditor.onDidContentSizeChange(onContentSizeChange)
110
+ editableEditor = modifiedEditor
111
+ }
112
+ else {
113
+ const editor = monaco.editor.create(container.value!, {
114
+ model,
115
+ lineDecorationsWidth: 0,
116
+ ...commonOptions,
117
+ })
118
+ editor.onDidContentSizeChange((e) => {
119
+ const newHeight = e.contentHeight + 4
120
+ initialHeight.value ??= newHeight
121
+ contentHeight.value = newHeight
122
+ nextTick(() => editableEditor.layout())
123
+ })
124
+ editableEditor = editor
125
+ }
126
+ if (props.ata) {
127
+ ata(editableEditor.getValue())
128
+ editableEditor.onDidChangeModelContent(debounce(1000, () => {
129
+ ata(editableEditor.getValue())
130
+ }))
131
+ }
132
+ const originalLayoutContentWidget = editableEditor.layoutContentWidget.bind(editableEditor)
133
+ editableEditor.layoutContentWidget = (widget: any) => {
134
+ originalLayoutContentWidget(widget)
135
+ const id = widget.getId()
136
+ if (id === 'editor.contrib.resizableContentHoverWidget') {
137
+ widget._resizableNode.domNode.style.transform = widget._positionPreference === 1
138
+ ? /* ABOVE */ `translateY(calc(100% * (var(--slidev-slide-scale) - 1)))`
139
+ : /* BELOW */ `` // reset
117
140
  }
118
- return
119
141
  }
120
- if (payload.type !== 'slidev-monaco')
121
- return
122
- if (payload.data?.height)
123
- editorHeight.value = payload.data?.height
124
- if (payload?.data?.code && code.value !== payload.data.code)
125
- code.value = payload.data.code
126
- if (payload?.data?.diff && diff.value !== payload.data.diff)
127
- diff.value = payload.data.diff
128
142
  })
129
143
  </script>
130
144
 
131
145
  <template>
132
- <iframe ref="iframe" class="text-base w-full rounded" :style="{ height }" />
146
+ <div ref="outer" class="slidev-monaco-container" :style="{ height }">
147
+ <div ref="container" class="absolute inset-0.5" />
148
+ </div>
133
149
  </template>
@@ -2,16 +2,18 @@
2
2
  import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
3
3
  import type { KeyedTokensInfo } from 'shiki-magic-move/types'
4
4
  import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
5
+ import { decompressFromBase64 } from 'lz-string'
5
6
  import { useSlideContext } from '../context'
6
7
  import { makeId } from '../logic/utils'
7
8
 
8
9
  import 'shiki-magic-move/style.css'
9
10
 
10
11
  const props = defineProps<{
11
- steps: KeyedTokensInfo[]
12
+ stepsLz: string
12
13
  at?: string | number
13
14
  }>()
14
15
 
16
+ const steps = JSON.parse(decompressFromBase64(props.stepsLz)) as KeyedTokensInfo[]
15
17
  const { $clicksContext: clicks, $scale: scale } = useSlideContext()
16
18
  const id = makeId()
17
19
  const index = ref(0)
@@ -24,14 +26,14 @@ onMounted(() => {
24
26
  if (!clicks || clicks.disabled)
25
27
  return
26
28
 
27
- const { start, end, delta } = clicks.resolve(props.at || '+1', props.steps.length - 1)
29
+ const { start, end, delta } = clicks.resolve(props.at || '+1', steps.length - 1)
28
30
  clicks.register(id, { max: end, delta })
29
31
 
30
32
  watchEffect(() => {
31
33
  if (clicks.disabled)
32
- index.value = props.steps.length - 1
34
+ index.value = steps.length - 1
33
35
  else
34
- index.value = Math.min(Math.max(0, clicks.current - start + 1), props.steps.length - 1)
36
+ index.value = Math.min(Math.max(0, clicks.current - start + 1), steps.length - 1)
35
37
  })
36
38
  })
37
39
  </script>
@@ -12,7 +12,10 @@ const props = defineProps<{
12
12
  autoScroll?: boolean
13
13
  }>()
14
14
 
15
- defineEmits(['click'])
15
+ const emit = defineEmits<{
16
+ (type: 'markerDblclick', e: MouseEvent, clicks: number): void
17
+ (type: 'markerClick', e: MouseEvent, clicks: number): void
18
+ }>()
16
19
 
17
20
  const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
18
21
  const noteDisplay = ref<HTMLElement | null>(null)
@@ -21,16 +24,13 @@ const CLASS_FADE = 'slidev-note-fade'
21
24
  const CLASS_MARKER = 'slidev-note-click-mark'
22
25
 
23
26
  function highlightNote() {
24
- if (!noteDisplay.value || !withClicks.value || props.clicksContext?.current == null)
27
+ if (!noteDisplay.value || !withClicks.value)
25
28
  return
26
29
 
27
- const current = +props.clicksContext?.current ?? CLICKS_MAX
30
+ const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
31
+
32
+ const current = +(props.clicksContext?.current ?? CLICKS_MAX)
28
33
  const disabled = current < 0 || current >= CLICKS_MAX
29
- if (disabled) {
30
- Array.from(noteDisplay.value.querySelectorAll('*'))
31
- .forEach(el => el.classList.remove(CLASS_FADE))
32
- return
33
- }
34
34
 
35
35
  const nodeToIgnores = new Set<Element>()
36
36
  function ignoreParent(node: Element) {
@@ -41,7 +41,6 @@ function highlightNote() {
41
41
  ignoreParent(node.parentElement)
42
42
  }
43
43
 
44
- const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
45
44
  const markersMap = new Map<number, HTMLElement>()
46
45
 
47
46
  // Convert all sibling text nodes to spans, so we attach classes to them
@@ -78,25 +77,36 @@ function highlightNote() {
78
77
 
79
78
  // Apply
80
79
  for (const [count, els] of segments) {
81
- els.forEach(el => el.classList.toggle(
82
- CLASS_FADE,
83
- nodeToIgnores.has(el)
84
- ? false
85
- : count !== current,
86
- ))
80
+ if (disabled) {
81
+ els.forEach(el => el.classList.remove(CLASS_FADE))
82
+ }
83
+ else {
84
+ els.forEach(el => el.classList.toggle(
85
+ CLASS_FADE,
86
+ nodeToIgnores.has(el)
87
+ ? false
88
+ : count !== current,
89
+ ))
90
+ }
87
91
  }
88
92
 
89
93
  for (const [clicks, marker] of markersMap) {
90
94
  marker.classList.remove(CLASS_FADE)
91
- marker.classList.toggle(`${CLASS_MARKER}-past`, clicks < current)
92
- marker.classList.toggle(`${CLASS_MARKER}-active`, clicks === current)
93
- marker.classList.toggle(`${CLASS_MARKER}-next`, clicks === current + 1)
94
- marker.classList.toggle(`${CLASS_MARKER}-future`, clicks > current + 1)
95
- marker.addEventListener('dblclick', (e) => {
95
+ marker.classList.toggle(`${CLASS_MARKER}-past`, disabled ? false : clicks < current)
96
+ marker.classList.toggle(`${CLASS_MARKER}-active`, disabled ? false : clicks === current)
97
+ marker.classList.toggle(`${CLASS_MARKER}-next`, disabled ? false : clicks === current + 1)
98
+ marker.classList.toggle(`${CLASS_MARKER}-future`, disabled ? false : clicks > current + 1)
99
+ marker.ondblclick = (e) => {
100
+ emit('markerDblclick', e, clicks)
101
+ if (e.defaultPrevented)
102
+ return
96
103
  props.clicksContext!.current = clicks
97
104
  e.stopPropagation()
98
105
  e.stopImmediatePropagation()
99
- })
106
+ }
107
+ marker.onclick = (e) => {
108
+ emit('markerClick', e, clicks)
109
+ }
100
110
 
101
111
  if (props.autoScroll && clicks === current)
102
112
  marker.scrollIntoView({ block: 'center', behavior: 'smooth' })
@@ -124,14 +134,12 @@ onMounted(() => {
124
134
  ref="noteDisplay"
125
135
  class="prose overflow-auto outline-none slidev-note"
126
136
  :class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
127
- @click="$emit('click')"
128
137
  v-html="noteHtml"
129
138
  />
130
139
  <div
131
140
  v-else-if="note"
132
141
  class="prose overflow-auto outline-none slidev-note"
133
142
  :class="props.class"
134
- @click="$emit('click')"
135
143
  >
136
144
  <p v-text="note" />
137
145
  </div>
@@ -139,7 +147,6 @@ onMounted(() => {
139
147
  v-else
140
148
  class="prose overflow-auto outline-none opacity-50 italic select-none slidev-note"
141
149
  :class="props.class"
142
- @click="$emit('click')"
143
150
  >
144
151
  <p v-text="props.placeholder || 'No notes.'" />
145
152
  </div>
@@ -30,9 +30,12 @@ const props = defineProps({
30
30
  },
31
31
  })
32
32
 
33
- const emit = defineEmits([
34
- 'update:editing',
35
- ])
33
+ const emit = defineEmits<{
34
+ (type: 'update:editing', value: boolean): void
35
+ (type: 'markerDblclick', e: MouseEvent, clicks: number): void
36
+ (type: 'markerClick', e: MouseEvent, clicks: number): void
37
+ }>()
38
+
36
39
  const editing = useVModel(props, 'editing', emit, { passive: true })
37
40
 
38
41
  const { info, update } = useDynamicSlideInfo(props.no)
@@ -40,9 +43,12 @@ const { info, update } = useDynamicSlideInfo(props.no)
40
43
  const note = ref('')
41
44
  let timer: any
42
45
 
46
+ // Send back the note on changes
43
47
  const { ignoreUpdates } = ignorableWatch(
44
48
  note,
45
49
  (v) => {
50
+ if (!editing.value)
51
+ return
46
52
  const id = props.no
47
53
  clearTimeout(timer)
48
54
  timer = setTimeout(() => {
@@ -51,46 +57,44 @@ const { ignoreUpdates } = ignorableWatch(
51
57
  },
52
58
  )
53
59
 
60
+ // Update note value when info changes
54
61
  watch(
55
- info,
56
- (v) => {
62
+ () => info.value?.note,
63
+ (value = '') => {
57
64
  if (editing.value)
58
65
  return
59
66
  clearTimeout(timer)
60
67
  ignoreUpdates(() => {
61
- note.value = v?.note || ''
68
+ note.value = value
62
69
  })
63
70
  },
64
71
  { immediate: true, flush: 'sync' },
65
72
  )
66
73
 
67
- const input = ref<HTMLTextAreaElement>()
74
+ const inputEl = ref<HTMLTextAreaElement>()
75
+ const inputHeight = ref<number | null>()
68
76
 
69
77
  watchEffect(() => {
70
78
  if (editing.value)
71
- input.value?.focus()
79
+ inputEl.value?.focus()
72
80
  })
73
81
 
74
- onClickOutside(input, () => {
82
+ onClickOutside(inputEl, () => {
75
83
  editing.value = false
76
84
  })
77
85
 
78
- function calculateHeight() {
79
- if (!props.autoHeight || !input.value || !editing.value)
86
+ function calculateEditorHeight() {
87
+ if (!props.autoHeight || !inputEl.value || !editing.value)
80
88
  return
81
- if (input.value.scrollHeight > input.value.clientHeight)
82
- input.value.style.height = `${input.value.scrollHeight}px`
89
+ if (inputEl.value.scrollHeight > inputEl.value.clientHeight)
90
+ inputEl.value.style.height = `${inputEl.value.scrollHeight}px`
83
91
  }
84
92
 
85
- const inputHeight = ref<number | null>()
86
-
87
93
  watch(
88
94
  note,
89
- () => {
90
- nextTick(() => {
91
- calculateHeight()
92
- })
93
- },
95
+ () => nextTick(() => {
96
+ calculateEditorHeight()
97
+ }),
94
98
  { flush: 'post', immediate: true },
95
99
  )
96
100
  </script>
@@ -105,10 +109,12 @@ watch(
105
109
  :note-html="info?.noteHTML"
106
110
  :clicks-context="clicksContext"
107
111
  :auto-scroll="!autoHeight"
112
+ @marker-click="(e, clicks) => emit('markerClick', e, clicks)"
113
+ @marker-dblclick="(e, clicks) => emit('markerDblclick', e, clicks)"
108
114
  />
109
115
  <textarea
110
116
  v-else
111
- ref="input"
117
+ ref="inputEl"
112
118
  v-model="note"
113
119
  class="prose resize-none overflow-auto outline-none bg-transparent block border-primary border-2"
114
120
  style="line-height: 1.75;"
package/logic/nav.ts CHANGED
@@ -135,16 +135,13 @@ export function go(page: number | string, clicks?: number) {
135
135
  export function useSwipeControls(root: Ref<HTMLElement | undefined>) {
136
136
  const swipeBegin = ref(0)
137
137
  const { direction, distanceX, distanceY } = usePointerSwipe(root, {
138
- onSwipeStart(e) {
139
- if (e.pointerType !== 'touch')
140
- return
138
+ pointerTypes: ['touch'],
139
+ onSwipeStart() {
141
140
  if (isDrawing.value)
142
141
  return
143
142
  swipeBegin.value = timestamp()
144
143
  },
145
- onSwipeEnd(e) {
146
- if (e.pointerType !== 'touch')
147
- return
144
+ onSwipeEnd() {
148
145
  if (!swipeBegin.value)
149
146
  return
150
147
  if (isDrawing.value)
@@ -1,17 +1,16 @@
1
1
  import mermaid from 'mermaid/dist/mermaid.esm.mjs'
2
- import { customAlphabet } from 'nanoid'
3
- import { decode } from 'js-base64'
2
+ import { decompressFromBase64 } from 'lz-string'
4
3
  import { clearUndefined } from '@antfu/utils'
5
4
  import setupMermaid from '../setup/mermaid'
5
+ import { makeId } from '../logic/utils'
6
6
 
7
7
  mermaid.startOnLoad = false
8
8
  mermaid.initialize({ startOnLoad: false })
9
9
 
10
- const nanoid = customAlphabet('abcedfghicklmn', 10)
11
10
  const cache = new Map<string, string>()
12
11
 
13
- export async function renderMermaid(encoded: string, options: any) {
14
- const key = encoded + JSON.stringify(options)
12
+ export async function renderMermaid(lzEncoded: string, options: any) {
13
+ const key = lzEncoded + JSON.stringify(options)
15
14
  const _cache = cache.get(key)
16
15
  if (_cache)
17
16
  return _cache
@@ -21,8 +20,8 @@ export async function renderMermaid(encoded: string, options: any) {
21
20
  ...clearUndefined(setupMermaid() || {}),
22
21
  ...clearUndefined(options),
23
22
  })
24
- const code = decode(encoded)
25
- const id = nanoid()
23
+ const code = decompressFromBase64(lzEncoded)
24
+ const id = makeId()
26
25
  const { svg } = await mermaid.render(id, code)
27
26
  cache.set(key, svg)
28
27
  return svg
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.48.0-beta.14",
4
+ "version": "0.48.0-beta.16",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -29,8 +29,10 @@
29
29
  "@antfu/utils": "^0.7.7",
30
30
  "@iconify-json/carbon": "^1.1.30",
31
31
  "@iconify-json/ph": "^1.1.11",
32
+ "@shikijs/monaco": "^1.1.7",
32
33
  "@shikijs/vitepress-twoslash": "^1.1.7",
33
34
  "@slidev/rough-notation": "^0.1.0",
35
+ "@typescript/ata": "^0.9.4",
34
36
  "@unhead/vue": "^1.8.10",
35
37
  "@unocss/reset": "^0.58.5",
36
38
  "@vueuse/core": "^10.8.0",
@@ -42,21 +44,22 @@
42
44
  "file-saver": "^2.0.5",
43
45
  "floating-vue": "^5.2.2",
44
46
  "fuse.js": "^7.0.0",
45
- "js-base64": "^3.7.7",
46
47
  "js-yaml": "^4.1.0",
47
48
  "katex": "^0.16.9",
49
+ "lz-string": "^1.5.0",
48
50
  "mermaid": "^10.8.0",
49
- "monaco-editor": "^0.37.1",
51
+ "monaco-editor": "^0.46.0",
50
52
  "nanoid": "^5.0.6",
51
53
  "prettier": "^3.2.5",
52
54
  "recordrtc": "^5.6.2",
53
55
  "resolve": "^1.22.8",
56
+ "shiki": "^1.1.7",
54
57
  "shiki-magic-move": "^0.1.0",
55
58
  "unocss": "^0.58.5",
56
59
  "vue": "^3.4.20",
57
60
  "vue-router": "^4.3.0",
58
- "@slidev/types": "0.48.0-beta.14",
59
- "@slidev/parser": "0.48.0-beta.14"
61
+ "@slidev/parser": "0.48.0-beta.16",
62
+ "@slidev/types": "0.48.0-beta.16"
60
63
  },
61
64
  "devDependencies": {
62
65
  "vite": "^5.1.4"
@@ -12,7 +12,7 @@ import SlideContainer from '../internals/SlideContainer.vue'
12
12
  import SlideWrapper from '../internals/SlideWrapper'
13
13
  import DrawingPreview from '../internals/DrawingPreview.vue'
14
14
  import IconButton from '../internals/IconButton.vue'
15
- import NoteEditor from '../internals/NoteEditor.vue'
15
+ import NoteEditable from '../internals/NoteEditable.vue'
16
16
  import OverviewClicksSlider from '../internals/OverviewClicksSlider.vue'
17
17
  import { CLICKS_MAX } from '../constants'
18
18
 
@@ -80,6 +80,15 @@ function scrollToSlide(idx: number) {
80
80
  el.scrollIntoView({ behavior: 'smooth', block: 'start' })
81
81
  }
82
82
 
83
+ function onMarkerClick(e: MouseEvent, clicks: number, route: RouteRecordRaw) {
84
+ const ctx = getClicksContext(route)
85
+ if (ctx.current === clicks)
86
+ ctx.current = CLICKS_MAX
87
+ else
88
+ ctx.current = clicks
89
+ e.preventDefault()
90
+ }
91
+
83
92
  onMounted(() => {
84
93
  nextTick(() => {
85
94
  checkActiveBlocks()
@@ -192,7 +201,7 @@ onMounted(() => {
192
201
  <carbon:pen />
193
202
  </IconButton>
194
203
  </div>
195
- <NoteEditor
204
+ <NoteEditable
196
205
  :no="idx"
197
206
  class="max-w-250 w-250 text-lg rounded p3"
198
207
  :auto-height="true"
@@ -200,6 +209,7 @@ onMounted(() => {
200
209
  :clicks-context="getClicksContext(route)"
201
210
  @dblclick="edittingNote !== idx ? edittingNote = idx : null"
202
211
  @update:editing="edittingNote = null"
212
+ @marker-click="(e, clicks) => onMarkerClick(e, clicks, route)"
203
213
  />
204
214
  <div
205
215
  v-if="wordCounts[idx] > 0"
@@ -15,7 +15,7 @@ import SlideWrapper from '../internals/SlideWrapper'
15
15
  import SlideContainer from '../internals/SlideContainer.vue'
16
16
  import NavControls from '../internals/NavControls.vue'
17
17
  import QuickOverview from '../internals/QuickOverview.vue'
18
- import NoteEditor from '../internals/NoteEditor.vue'
18
+ import NoteEditable from '../internals/NoteEditable.vue'
19
19
  import NoteStatic from '../internals/NoteStatic.vue'
20
20
  import Goto from '../internals/Goto.vue'
21
21
  import SlidesShow from '../internals/SlidesShow.vue'
@@ -130,7 +130,7 @@ onMounted(() => {
130
130
  <SideEditor />
131
131
  </div>
132
132
  <div v-else class="grid-section note grid grid-rows-[1fr_min-content] overflow-hidden">
133
- <NoteEditor
133
+ <NoteEditable
134
134
  v-if="__DEV__"
135
135
  :key="`edit-${currentSlideId}`"
136
136
  v-model:editing="notesEditing"
package/setup/monaco.ts CHANGED
@@ -1,52 +1,115 @@
1
- import { getCurrentInstance, onMounted } from 'vue'
2
- import * as monaco from 'monaco-editor'
3
1
  import { createSingletonPromise } from '@antfu/utils'
4
2
  import type { MonacoSetupReturn } from '@slidev/types'
3
+ import * as monaco from 'monaco-editor'
4
+ import { watchEffect } from 'vue'
5
+ import { setupTypeAcquisition } from '@typescript/ata'
6
+ import ts from 'typescript'
7
+
8
+ import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
9
+ import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
10
+ import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
11
+ import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
12
+ import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
13
+
14
+ // @ts-expect-error missing types
15
+ import { ContextViewService } from 'monaco-editor/esm/vs/platform/contextview/browser/contextViewService'
16
+
17
+ // @ts-expect-error missing types
18
+ import { SyncDescriptor } from 'monaco-editor/esm/vs/platform/instantiation/common/descriptors'
19
+
20
+ // @ts-expect-error missing types
21
+ import { StandaloneServices } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneServices'
22
+
23
+ import { isDark } from '../logic/dark'
24
+ import configs from '#slidev/configs'
5
25
 
6
26
  /* __imports__ */
7
27
 
28
+ window.MonacoEnvironment = {
29
+ getWorker(_, label) {
30
+ if (label === 'json')
31
+ return new JsonWorker()
32
+ if (label === 'css' || label === 'scss' || label === 'less')
33
+ return new CssWorker()
34
+ if (label === 'html' || label === 'handlebars' || label === 'razor')
35
+ return new HtmlWorker()
36
+ if (label === 'typescript' || label === 'javascript')
37
+ return new TsWorker()
38
+ return new EditorWorker()
39
+ },
40
+ }
41
+
42
+ class ContextViewService2 extends ContextViewService {
43
+ showContextView(...args: any) {
44
+ super.showContextView(...args)
45
+ // @ts-expect-error missing types
46
+ const contextView = this.contextView.view as HTMLElement
47
+ contextView.style.left = `calc(${contextView.style.left} / var(--slidev-slide-scale))`
48
+ contextView.style.top = `calc(${contextView.style.top} / var(--slidev-slide-scale))`
49
+ // Reset the scale to 1. Otherwise, the sub-menu will be in the wrong position.
50
+ contextView.style.transform = `scale(calc(1 / var(--slidev-slide-scale)))`
51
+ contextView.style.transformOrigin = '0 0'
52
+ }
53
+ }
54
+
8
55
  const setup = createSingletonPromise(async () => {
9
- monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
10
- ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
11
- noUnusedLocals: false,
12
- noUnusedParameters: false,
13
- allowUnreachableCode: true,
14
- allowUnusedLabels: true,
56
+ // Initialize services first, otherwise we can't override them.
57
+ StandaloneServices.initialize({
58
+ contextViewService: new SyncDescriptor(ContextViewService2, [], true),
59
+ })
60
+
61
+ const defaults = monaco.languages.typescript.typescriptDefaults
62
+
63
+ defaults.setCompilerOptions({
64
+ ...defaults.getCompilerOptions(),
15
65
  strict: true,
66
+ moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
67
+ module: monaco.languages.typescript.ModuleKind.ESNext,
16
68
  })
17
69
 
18
- await Promise.all([
19
- // load workers
20
- (async () => {
21
- const [
22
- { default: EditorWorker },
23
- { default: JsonWorker },
24
- { default: CssWorker },
25
- { default: HtmlWorker },
26
- { default: TsWorker },
27
- ] = await Promise.all([
28
- import('monaco-editor/esm/vs/editor/editor.worker?worker'),
29
- import('monaco-editor/esm/vs/language/json/json.worker?worker'),
30
- import('monaco-editor/esm/vs/language/css/css.worker?worker'),
31
- import('monaco-editor/esm/vs/language/html/html.worker?worker'),
32
- import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
33
- ])
34
-
35
- window.MonacoEnvironment = {
36
- getWorker(_: any, label: string) {
37
- if (label === 'json')
38
- return new JsonWorker()
39
- if (label === 'css' || label === 'scss' || label === 'less')
40
- return new CssWorker()
41
- if (label === 'html' || label === 'handlebars' || label === 'razor')
42
- return new HtmlWorker()
43
- if (label === 'typescript' || label === 'javascript')
44
- return new TsWorker()
45
- return new EditorWorker()
70
+ // Load types from server
71
+ import('#slidev/monaco-types')
72
+
73
+ const ata = configs.monacoTypesSource === 'cdn'
74
+ ? setupTypeAcquisition({
75
+ projectName: 'TypeScript Playground',
76
+ typescript: ts as any, // Version mismatch. No problem found so far.
77
+ logger: console,
78
+ delegate: {
79
+ receivedFile: (code: string, path: string) => {
80
+ defaults.addExtraLib(code, `file://${path}`)
81
+ const uri = monaco.Uri.file(path)
82
+ if (monaco.editor.getModel(uri) === null)
83
+ monaco.editor.createModel(code, 'javascript', uri)
84
+ },
85
+ progress: (downloaded: number, total: number) => {
86
+ // eslint-disable-next-line no-console
87
+ console.debug(`[Typescript ATA] ${downloaded} / ${total}`)
46
88
  },
47
- }
48
- })(),
49
- ])
89
+ },
90
+ })
91
+ : () => { }
92
+
93
+ // monaco.languages.register({ id: 'vue' })
94
+ monaco.languages.register({ id: 'typescript' })
95
+ monaco.languages.register({ id: 'javascript' })
96
+
97
+ const { shiki, themes, shikiToMonaco } = await import('#slidev/shiki')
98
+ const highlighter = await shiki
99
+
100
+ // Use Shiki to highlight Monaco
101
+ shikiToMonaco(highlighter, monaco)
102
+
103
+ if (typeof themes === 'string') {
104
+ monaco.editor.setTheme(themes)
105
+ }
106
+ else {
107
+ watchEffect(() => {
108
+ monaco.editor.setTheme(isDark.value
109
+ ? themes.dark || 'vitesse-dark'
110
+ : themes.light || 'vitesse-light')
111
+ })
112
+ }
50
113
 
51
114
  // @ts-expect-error injected in runtime
52
115
  // eslint-disable-next-line unused-imports/no-unused-vars
@@ -56,15 +119,11 @@ const setup = createSingletonPromise(async () => {
56
119
 
57
120
  /* __async_injections__ */
58
121
 
59
- if (getCurrentInstance())
60
- await new Promise<void>(resolve => onMounted(resolve))
61
-
62
122
  return {
63
123
  monaco,
124
+ ata,
64
125
  ...injection_return,
65
126
  }
66
127
  })
67
128
 
68
129
  export default setup
69
-
70
- setup()
package/setup/root.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /* __imports__ */
2
2
  import { watch } from 'vue'
3
3
  import { useHead } from '@unhead/vue'
4
- import { nanoid } from 'nanoid'
5
4
  import { configs } from '../env'
6
5
  import { initSharedState, onPatch, patch } from '../state/shared'
7
6
  import { initDrawingState } from '../state/drawings'
@@ -9,6 +8,7 @@ import { clicksContext, currentPage, getPath, isNotesViewer, isPresenter } from
9
8
  import { router } from '../routes'
10
9
  import { TRUST_ORIGINS } from '../constants'
11
10
  import { skipTransition } from '../composables/hmr'
11
+ import { makeId } from '../logic/utils'
12
12
 
13
13
  export default function setupRoot() {
14
14
  // @ts-expect-error injected in runtime
@@ -25,7 +25,7 @@ export default function setupRoot() {
25
25
  initSharedState(`${title} - shared`)
26
26
  initDrawingState(`${title} - drawings`)
27
27
 
28
- const id = `${location.origin}_${nanoid()}`
28
+ const id = `${location.origin}_${makeId()}`
29
29
 
30
30
  // update shared state
31
31
  function updateSharedState() {
package/shim.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- // with unplugin-vue-markdown, markdowns can be treat as Vue components
2
1
  declare module '*.md' {
2
+ // with unplugin-vue-markdown, markdowns can be treat as Vue components
3
3
  import type { ComponentOptions } from 'vue'
4
4
 
5
5
  const component: ComponentOptions
@@ -0,0 +1,27 @@
1
+ div[widgetid='messageoverlay'] {
2
+ transform: translateY(calc(100% * (var(--slidev-slide-scale) - 1)));
3
+ }
4
+
5
+ .slidev-monaco-container {
6
+ position: relative;
7
+ margin: var(--slidev-code-margin);
8
+ padding: var(--slidev-code-padding);
9
+ line-height: var(--slidev-code-line-height);
10
+ border-radius: var(--slidev-code-radius);
11
+ background: var(--slidev-code-background);
12
+ }
13
+
14
+ .slidev-monaco-container .monaco-editor {
15
+ --monaco-monospace-font: var(--slidev-code-font-family);
16
+ --vscode-editor-background: var(--slidev-code-background);
17
+ --vscode-editorGutter-background: var(--slidev-code-background);
18
+ }
19
+
20
+ /** Revert styles */
21
+ .slidev-monaco-container .monaco-editor a {
22
+ border-bottom: none;
23
+ }
24
+
25
+ .slidev-monaco-container .monaco-editor a:hover {
26
+ border-bottom: none;
27
+ }
@@ -1,28 +0,0 @@
1
- html,
2
- body,
3
- #container {
4
- padding: 0;
5
- margin: 0;
6
- background: var(--slidev-code-background);
7
- width: 100%;
8
- height: 200%;
9
- }
10
-
11
- #container {
12
- padding: var(--slidev-code-padding);
13
- margin: var(--slidev-code-margin);
14
- border-radius: var(--slidev-code-radius);
15
- }
16
-
17
- .monaco-editor .monaco-hover {
18
- border-radius: var(--slidev-code-radius);
19
- overflow: hidden;
20
- border: none;
21
- outline: none;
22
- }
23
-
24
- .monaco-editor .lines-content,
25
- .monaco-editor .view-line,
26
- .monaco-editor .view-lines {
27
- user-select: none;
28
- }
@@ -1,7 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <body>
4
- <div id="container"></div>
5
- <script type="module" src="./index.ts"></script>
6
- </body>
7
- </html>
@@ -1,260 +0,0 @@
1
- import '/@slidev/styles'
2
- import './index.css'
3
-
4
- import type * as monaco from 'monaco-editor'
5
- import { formatCode } from '../../setup/prettier'
6
- import setupMonaco from '../../setup/monaco'
7
- import '/@slidev/monaco-types'
8
-
9
- const url = new URL(location.href)
10
- const props = {
11
- id: url.searchParams.get('id'),
12
- code: '',
13
- diff: '',
14
- lang: url.searchParams.get('lang') ?? 'typescript',
15
- readonly: false,
16
- lineNumbers: url.searchParams.get('lineNumbers') ?? 'off',
17
- dark: false,
18
- style: '',
19
- editorOptions: {},
20
- }
21
-
22
- const styleObject = document.createElement('style')
23
- let originalEditor: monaco.editor.IStandaloneCodeEditor
24
- let modifiedEditor: monaco.editor.IStandaloneCodeEditor
25
- let format: () => void = () => { }
26
- let update: () => void = () => { }
27
-
28
- document.body.appendChild(styleObject)
29
-
30
- function lang() {
31
- switch (props.lang) {
32
- case 'ts':
33
- case 'tsx':
34
- return 'typescript'
35
- case 'jsx':
36
- case 'js':
37
- return 'javascript'
38
- default:
39
- return props.lang
40
- }
41
- }
42
-
43
- function ext() {
44
- switch (lang()) {
45
- case 'typescript':
46
- return 'ts'
47
- case 'javascript':
48
- return 'js'
49
- default:
50
- return lang()
51
- }
52
- }
53
-
54
- function post(data: any, type = 'slidev-monaco') {
55
- if (window.parent === window)
56
- return
57
-
58
- window.parent.postMessage(
59
- {
60
- type,
61
- id: props.id,
62
- data,
63
- },
64
- location.origin,
65
- )
66
- }
67
-
68
- async function start() {
69
- const { monaco, theme = {}, editorOptions = {} } = await setupMonaco()
70
-
71
- const style = getComputedStyle(document.documentElement)
72
- const container = document.getElementById('container')!
73
-
74
- const model = monaco.editor.createModel(
75
- props.code,
76
- lang(),
77
- monaco.Uri.parse(`file:///root/${Date.now()}.${ext()}`),
78
- )
79
-
80
- if (url.searchParams.get('diff')) {
81
- // Diff editor
82
- const diffModel = monaco.editor.createModel(
83
- props.diff,
84
- lang(),
85
- monaco.Uri.parse(`file:///root/${Date.now()}.${ext()}`),
86
- )
87
- const monacoEditor = monaco.editor.createDiffEditor(container, {
88
- fontSize: +style.getPropertyValue('--slidev-code-font-size').replace(/px/g, ''),
89
- fontFamily: style.getPropertyValue('--slidev-code-font-family'),
90
- lineHeight: +style.getPropertyValue('--slidev-code-line-height').replace(/px/g, ''),
91
- lineDecorationsWidth: 0,
92
- lineNumbersMinChars: 0,
93
- scrollBeyondLastLine: false,
94
- scrollBeyondLastColumn: 0,
95
- automaticLayout: true,
96
- readOnly: props.readonly,
97
- theme: 'vitesse-dark',
98
- lineNumbers: props.lineNumbers as any,
99
- glyphMargin: false,
100
- scrollbar: {
101
- useShadows: false,
102
- vertical: 'hidden',
103
- horizontal: 'hidden',
104
- },
105
- overviewRulerLanes: 0,
106
- minimap: { enabled: false },
107
- enableSplitViewResizing: false,
108
- renderOverviewRuler: false,
109
- // renderSideBySide: false,
110
- ...editorOptions,
111
- })
112
- monacoEditor.setModel({
113
- original: model,
114
- modified: diffModel,
115
- })
116
- originalEditor = monacoEditor.getOriginalEditor()
117
- modifiedEditor = monacoEditor.getModifiedEditor()
118
-
119
- format = async () => {
120
- model.setValue((await formatCode(props.code, lang())).trim())
121
- diffModel.setValue((await formatCode(props.diff, lang())).trim())
122
- }
123
-
124
- // ctrl+s to format
125
- originalEditor.onKeyDown((e) => {
126
- if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
127
- e.preventDefault()
128
- format()
129
- }
130
- })
131
- modifiedEditor.onKeyDown((e) => {
132
- if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
133
- e.preventDefault()
134
- format()
135
- }
136
- })
137
-
138
- update = () => {
139
- monaco.editor.setTheme(props.dark
140
- ? (theme.dark || 'vitesse-dark')
141
- : (theme.light || 'vitesse-light'))
142
- styleObject.innerHTML = `:root { ${props.style} }`
143
-
144
- if (originalEditor.getValue().toString() !== props.code) {
145
- const selection = originalEditor.getSelection()
146
- originalEditor.setValue(props.code)
147
- if (selection)
148
- originalEditor.setSelection(selection)
149
- }
150
- originalEditor.updateOptions(props.editorOptions)
151
-
152
- if (modifiedEditor.getValue().toString() !== props.diff) {
153
- const selection = modifiedEditor.getSelection()
154
- modifiedEditor.setValue(props.diff)
155
- if (selection)
156
- modifiedEditor.setSelection(selection)
157
- }
158
- modifiedEditor.updateOptions(props.editorOptions)
159
- }
160
-
161
- diffModel.onDidChangeContent(() => {
162
- onCodeChange(diffModel.getValue().toString())
163
- })
164
-
165
- function onCodeChange(diff: string) {
166
- props.diff = diff
167
- post({ diff })
168
- }
169
- }
170
- else {
171
- // Normal editor
172
- originalEditor = monaco.editor.create(container, {
173
- model,
174
- tabSize: 2,
175
- insertSpaces: true,
176
- detectIndentation: false,
177
- folding: false,
178
- fontSize: +style.getPropertyValue('--slidev-code-font-size').replace(/px/g, ''),
179
- fontFamily: style.getPropertyValue('--slidev-code-font-family'),
180
- lineHeight: +style.getPropertyValue('--slidev-code-line-height').replace(/px/g, ''),
181
- lineDecorationsWidth: 0,
182
- lineNumbersMinChars: 0,
183
- scrollBeyondLastLine: false,
184
- scrollBeyondLastColumn: 0,
185
- automaticLayout: true,
186
- readOnly: props.readonly,
187
- theme: 'vitesse-dark',
188
- lineNumbers: props.lineNumbers as any,
189
- glyphMargin: false,
190
- scrollbar: {
191
- useShadows: false,
192
- vertical: 'hidden',
193
- horizontal: 'hidden',
194
- },
195
- overviewRulerLanes: 0,
196
- minimap: { enabled: false },
197
- ...editorOptions,
198
- })
199
-
200
- format = async () => {
201
- model.setValue((await formatCode(props.code, lang())).trim())
202
- }
203
-
204
- // ctrl+s to format
205
- originalEditor.onKeyDown((e) => {
206
- if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
207
- e.preventDefault()
208
- format()
209
- }
210
- })
211
-
212
- update = () => {
213
- monaco.editor.setTheme(props.dark
214
- ? (theme.dark || 'vitesse-dark')
215
- : (theme.light || 'vitesse-light'))
216
- styleObject.innerHTML = `:root { ${props.style} }`
217
-
218
- if (originalEditor.getValue().toString() !== props.code) {
219
- const selection = originalEditor.getSelection()
220
- originalEditor.setValue(props.code)
221
- if (selection)
222
- originalEditor.setSelection(selection)
223
- }
224
- originalEditor.updateOptions(props.editorOptions)
225
- }
226
- }
227
-
228
- originalEditor.onDidContentSizeChange(() => {
229
- post({ height: Math.max(originalEditor.getContentHeight(), modifiedEditor?.getContentHeight() ?? 0) })
230
- })
231
-
232
- model.onDidChangeContent(() => {
233
- onCodeChange(model.getValue().toString())
234
- })
235
-
236
- function onCodeChange(code: string) {
237
- props.code = code
238
- post({ code })
239
- }
240
-
241
- update()
242
-
243
- post({}, 'slidev-monaco-loaded')
244
- }
245
-
246
- window.addEventListener('message', (payload) => {
247
- if (payload.source === window)
248
- return
249
- if (payload.origin !== location.origin)
250
- return
251
- if (typeof payload.data !== 'string')
252
- return
253
- const { type, data, id } = JSON.parse(payload.data)
254
- if (type === 'slidev-monaco' && id === props.id) {
255
- Object.assign(props, data)
256
- update()
257
- }
258
- })
259
-
260
- start()