@vestra-player/sdk 1.5.3 → 1.5.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.
- package/CHANGELOG.md +27 -0
- package/README.md +3 -1
- package/dist/plugin/testing.d.ts +9 -2
- package/dist/plugin/testing.js +9 -2
- package/dist/plugin/types.d.ts +68 -20
- package/dist/plugin/ui.js +22 -7
- package/llms-full.txt +7090 -0
- package/llms.txt +205 -0
- package/package.json +7 -2
- package/slint/vestra/markdown.slint +56 -5
package/llms.txt
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Vestra Player SDK
|
|
2
|
+
|
|
3
|
+
> @vestra-player/sdk 1.5.6: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. llms-full.txt, next to this file, has every declaration, the guides and the examples; L<n> below are its line numbers.
|
|
4
|
+
|
|
5
|
+
**How plugins run**
|
|
6
|
+
|
|
7
|
+
- Each plugin runs in its own Node.js process (Node.js 26, which the player ships) started by the player.
|
|
8
|
+
- The player loads the entry module and calls its **default export** `activate(plugin)`.
|
|
9
|
+
- Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle owned by `plugin` or a child scope, and is released automatically when the plugin stops.
|
|
10
|
+
- Plugins are trusted code.
|
|
11
|
+
- The API is the same on macOS and Windows.
|
|
12
|
+
|
|
13
|
+
**Rules**
|
|
14
|
+
|
|
15
|
+
- **Only the public SDK.** Import `@vestra-player/sdk` with `import type` only (a runtime import fails `vestra-plugin build`) and `@vestra-player/sdk/testing` only in tests. `@vestra-player/sdk/runtime` and the SDK's internal modules are for the player.
|
|
16
|
+
- **No other plugins.** Do not import another plugin's code, read its directory or data directory, or talk to it privately (shared files, sockets, messages). A plugin must load, run and stop while no other plugin is installed. Plugins share only what the API makes public: player state, timeline marks, menus, settings pages.
|
|
17
|
+
- **No system commands.** Do not run `open`, `sips`, `plutil`, `security`, `cmd`, `powershell` or similar. Use the API (`plugin.system` to open files and links, `plugin.clipboard`, `plugin.secrets`, `plugin.tools` for ffmpeg and ffprobe) or portable Node.js code. If the API lacks something every system needs, ask for it in the SDK repository.
|
|
18
|
+
- **macOS and Windows.** The same code runs on both: build paths with `node:path`, write package paths with `/`, and handle `unsupported` where a method documents a platform difference.
|
|
19
|
+
- **Your own id.** `<owner>.<name>`, where the owner is your GitHub user or organization. `vestra` and `vestra.*` are reserved for the plugins built into the player.
|
|
20
|
+
|
|
21
|
+
**Files**
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
my-plugin/
|
|
25
|
+
manifest.json identity and capabilities
|
|
26
|
+
main.ts the entry module
|
|
27
|
+
ui/hello.slint optional: user interface
|
|
28
|
+
tests/main.test.ts tests (node:test)
|
|
29
|
+
package.json dev dependency and scripts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**manifest.json** (every field; unknown fields and capabilities are rejected; id ≤ 128 bytes, name, author ≤ 64 characters, punchline ≤ 120, description ≤ 2000, tags ≤ 8 of ≤ 24 characters, license ≤ 64, sourceUrl ≤ 2048 bytes, icon PNG or JPEG ≤ 512 KB and 1024 px per side, readme, changelog ≤ 256 KB)
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"id": "me.hello",
|
|
37
|
+
"version": "0.1.0",
|
|
38
|
+
"apiVersion": "vestra/1",
|
|
39
|
+
"entry": "main.ts",
|
|
40
|
+
"capabilities": ["commands", "ui.osd"],
|
|
41
|
+
"minPlayerVersion": "0.4.1",
|
|
42
|
+
"name": "Hello",
|
|
43
|
+
"author": "Me",
|
|
44
|
+
"punchline": "Says hello from the Tools menu.",
|
|
45
|
+
"description": "A first plugin.\nIt adds Tools ▸ Hello, which shows an on-screen message.",
|
|
46
|
+
"tags": ["example"],
|
|
47
|
+
"icon": "assets/icon.png",
|
|
48
|
+
"readme": "README.md",
|
|
49
|
+
"changelog": "CHANGELOG.md",
|
|
50
|
+
"sourceUrl": "https://github.com/me/hello",
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**main.ts**
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type { Plugin } from "@vestra-player/sdk";
|
|
59
|
+
|
|
60
|
+
export default async function activate(plugin: Plugin) {
|
|
61
|
+
plugin.commands.add({
|
|
62
|
+
id: "hello",
|
|
63
|
+
path: ["Tools", "Hello"],
|
|
64
|
+
run: () => plugin.osd.show("Hello"),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Capabilities** (a call without its capability fails with `capability`)
|
|
70
|
+
|
|
71
|
+
- `commands`: `commands.add`, `host.run`
|
|
72
|
+
- `ui.window`: `windows.create`
|
|
73
|
+
- `ui.mount`: `panels.mount`, `panels.bottom` / `.sidebar` / `.backdrop`
|
|
74
|
+
- `ui.dialog`: `dialogs.*`
|
|
75
|
+
- `ui.osd`: `osd.show`, `osd.panel`
|
|
76
|
+
- `ui.drop`: `drops.provide`, `Panel.onDrop`
|
|
77
|
+
- `ui.theme`: `appearance.set`
|
|
78
|
+
- `ui.settings`: `settings.page`, `settings.show`
|
|
79
|
+
- `playback.read`: reading `player.state` at start, `refresh`, `stats`, `chapters`, `audioDevices`, `timeline.marks`
|
|
80
|
+
- `events`: live `player.state` / `onChange`, panel visibility, subtitle and window updates
|
|
81
|
+
- `playback.control`: `player.open`, `setPaused`, `seek`, `setRate`, … and `power.preventSleep`
|
|
82
|
+
- `producer.av`: `player.openStream` (with `playback.control`: the stream becomes the player's source)
|
|
83
|
+
- `processing.video`: `video.filter` (with `playback.read` and `events`), `video.view`, `video.render` (with `playback.read`)
|
|
84
|
+
- `processing.audio`: `audio.filter` (with `playback.read` and `events`)
|
|
85
|
+
- `subtitles.read` / `subtitles.control`: reading / changing subtitles
|
|
86
|
+
- `media.read` / `media.control`: `media.capture`, snapshot exports
|
|
87
|
+
- `source.resolve`: `resolvers.add`
|
|
88
|
+
- `plugins.manage`: `packages.validate`, `rollback`, `setEnabled`, `uninstall`
|
|
89
|
+
- `clipboard`: `clipboard.*`
|
|
90
|
+
- `window.control`: `window.setVideoArea`, `setAlwaysOnTop`, `setCursorAutoHide`, `setPictureInPicture`
|
|
91
|
+
- `bindings.manage`: `bindings.*`
|
|
92
|
+
- `timeline.marks`: `timeline.layer()`
|
|
93
|
+
- `secrets`: `secrets.*`
|
|
94
|
+
- No capability is needed for `tools`, `storage`, `system`, `ui.image`, `plugin.app`, reading `window.state` and `appearance.theme`, and `packages.list`.
|
|
95
|
+
|
|
96
|
+
**Test** (`tests/*.test.ts`, run by `vestra-plugin test`)
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// tests/main.test.ts
|
|
100
|
+
import assert from "node:assert/strict";
|
|
101
|
+
import test from "node:test";
|
|
102
|
+
import { startPlugin } from "@vestra-player/sdk/testing";
|
|
103
|
+
import activate from "../main.ts";
|
|
104
|
+
|
|
105
|
+
test("shows the position", async () => {
|
|
106
|
+
const { host, stop } = await startPlugin(activate, {
|
|
107
|
+
directory: new URL("..", import.meta.url), // reads manifest.json
|
|
108
|
+
});
|
|
109
|
+
host.setSource({ phase: "ready", position: 12, duration: 60 });
|
|
110
|
+
await host.settle();
|
|
111
|
+
await host.command("where").run();
|
|
112
|
+
assert.deepEqual(host.osd, ["12.0 s of 60.0 s"]);
|
|
113
|
+
await stop();
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Command line**
|
|
118
|
+
|
|
119
|
+
- `vestra-plugin check [dir...]`: Checks the manifest, then a strict type check of the plugin and its tests.
|
|
120
|
+
- `vestra-plugin test [dir...]`: Runs `node --test` on `tests/*.test.{ts,mjs}`.
|
|
121
|
+
- `vestra-plugin build [dir...] [--version x.y.z]`: Writes `dist/`: types stripped, files copied, manifest versioned.
|
|
122
|
+
- `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]`: Builds, then packs `dist/` as `<id>-<version>.vxp`.
|
|
123
|
+
- `vestra-player --plugin <dir>` runs a plugin from source, no build needed.
|
|
124
|
+
|
|
125
|
+
**Error codes** (`error.code`; branch on it, never on `message`)
|
|
126
|
+
|
|
127
|
+
- `stale`: A condition no longer held
|
|
128
|
+
- `capability`: The manifest does not declare the capability the call needs
|
|
129
|
+
- `invalid`: A bad argument
|
|
130
|
+
- `missing`: The target does not exist
|
|
131
|
+
- `busy`: The player is still doing a previous call of the same kind
|
|
132
|
+
- `conflict`: Another plugin owns the resource, or this plugin already uses the id
|
|
133
|
+
- `unavailable`: Not possible right now
|
|
134
|
+
- `unsupported`: Not possible on this system, build or source
|
|
135
|
+
- `failed`: The operation ran and failed
|
|
136
|
+
- `aborted`: An `AbortSignal` fired
|
|
137
|
+
- `timeout`: A time limit ran out
|
|
138
|
+
- `limit`: A player-wide limit was reached
|
|
139
|
+
- `reserved`: Packages.validate, PackageCandidate.trial, PackageCandidate.install: the id is `vestra` or starts with `vestra.`, which only the player's built-in plugins use
|
|
140
|
+
- `versionMismatch`: The package needs a newer player or another `apiVersion`
|
|
141
|
+
- `ui-prepare`: The player could not prepare a mounted panel or a settings page
|
|
142
|
+
- `ui`: The player could not apply a property to a tool window or a settings page, or could not build the Options window
|
|
143
|
+
- `disabled`: The plugin is being stopped
|
|
144
|
+
- Others: PluginError, llms-full.txt L974.
|
|
145
|
+
|
|
146
|
+
## Reference
|
|
147
|
+
|
|
148
|
+
- [Full reference](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt): in the package at node_modules/@vestra-player/sdk/llms-full.txt.
|
|
149
|
+
- [Tutorial: Getting started](https://vestrasys.github.io/vestra-player-sdk/documents/Getting_started.html): L9–280
|
|
150
|
+
- [Concept: Windows, panels, settings pages, dialogs and the OSD](https://vestrasys.github.io/vestra-player-sdk/documents/Windows__panels__settings_pages__dialogs_and_the_OSD.html): L281–626
|
|
151
|
+
- [How-to: Keep a slider responsive while dragging](https://vestrasys.github.io/vestra-player-sdk/documents/Keep_a_slider_responsive_while_dragging.html): L627–801
|
|
152
|
+
- [How-to: Remember a window's state between runs](https://vestrasys.github.io/vestra-player-sdk/documents/Remember_a_window_s_state_between_runs.html): L802–931
|
|
153
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L932–4153
|
|
154
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html) (TestHost, startPlugin): L4154–5084
|
|
155
|
+
|
|
156
|
+
## API
|
|
157
|
+
|
|
158
|
+
Where each module of `plugin` is declared:
|
|
159
|
+
|
|
160
|
+
- Plugin L1152, Scope L1023, PluginError L974, Disposable L947
|
|
161
|
+
- plugin.commands: Commands L1349
|
|
162
|
+
- plugin.windows: Windows L1554
|
|
163
|
+
- plugin.panels: Panels L2005
|
|
164
|
+
- plugin.dialogs: Dialogs L2287
|
|
165
|
+
- plugin.player: Player L2716
|
|
166
|
+
- plugin.video: VideoFilter L2964, VideoView L3027, RenderOptions L3069
|
|
167
|
+
- plugin.audio: AudioFilter L2988
|
|
168
|
+
- plugin.subtitles: Subtitles L3167
|
|
169
|
+
- plugin.media: MediaCapture L3325
|
|
170
|
+
- plugin.resolvers: Resolvers L3422
|
|
171
|
+
- plugin.packages: Packages L3643
|
|
172
|
+
- plugin.osd: Osd L2901
|
|
173
|
+
- plugin.host: Host L2864
|
|
174
|
+
- plugin.clipboard: Clipboard L3750
|
|
175
|
+
- plugin.power: Power L3770
|
|
176
|
+
- plugin.tools: Tools L3803
|
|
177
|
+
- plugin.storage: Storage L3846
|
|
178
|
+
- plugin.system: System L3869
|
|
179
|
+
- plugin.secrets: Secrets L3887
|
|
180
|
+
- plugin.window: MainWindow L2128
|
|
181
|
+
- plugin.app: App L2209
|
|
182
|
+
- plugin.bindings: Bindings L3720
|
|
183
|
+
- plugin.ui: Ui L1441
|
|
184
|
+
- plugin.timeline: Timeline L2085
|
|
185
|
+
- plugin.drops: Drops L1851
|
|
186
|
+
- plugin.appearance: Appearance L1880
|
|
187
|
+
- plugin.settings: Settings L1938
|
|
188
|
+
|
|
189
|
+
## Examples
|
|
190
|
+
|
|
191
|
+
Complete plugins with tests, in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples) and in llms-full.txt:
|
|
192
|
+
|
|
193
|
+
- hello: The README Quick start: one menu command and an on-screen message. L5498–5587
|
|
194
|
+
- blur: A tool window with one slider that blurs the playing video. L5588–5849
|
|
195
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L5850–6182
|
|
196
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L6183–6367
|
|
197
|
+
- panel-menu: A sidebar panel with its own right-click menu. L6368–6573
|
|
198
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L6574–6733
|
|
199
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L6734–6897
|
|
200
|
+
- markdown: A window that shows a Markdown document. L6898–7091
|
|
201
|
+
|
|
202
|
+
## Optional
|
|
203
|
+
|
|
204
|
+
- [Changelog](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md): changes per version and the player version each addition needs.
|
|
205
|
+
- Declarations with every doc comment: node_modules/@vestra-player/sdk/dist/plugin/types.d.ts, dist/plugin/testing.d.ts; Slint widgets: slint/vestra/*.slint.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vestra-player/sdk",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.6",
|
|
4
4
|
"description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"types": "./dist/player-runtime.d.ts",
|
|
25
25
|
"default": "./dist/player-runtime.js"
|
|
26
26
|
},
|
|
27
|
+
"./llms.txt": "./llms.txt",
|
|
28
|
+
"./llms-full.txt": "./llms-full.txt",
|
|
27
29
|
"./package.json": "./package.json"
|
|
28
30
|
},
|
|
29
31
|
"bin": {
|
|
@@ -35,7 +37,9 @@
|
|
|
35
37
|
"slint",
|
|
36
38
|
"README.md",
|
|
37
39
|
"CHANGELOG.md",
|
|
38
|
-
"LICENSE"
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"llms.txt",
|
|
42
|
+
"llms-full.txt"
|
|
39
43
|
],
|
|
40
44
|
"scripts": {
|
|
41
45
|
"build": "node scripts/build.mjs",
|
|
@@ -43,6 +47,7 @@
|
|
|
43
47
|
"doc-lint": "node scripts/doc-lint.mjs",
|
|
44
48
|
"docs": "typedoc",
|
|
45
49
|
"docs:check": "node scripts/check-docs.mjs",
|
|
50
|
+
"docs:llms": "node scripts/llms.mjs",
|
|
46
51
|
"examples": "node scripts/check-examples.mjs",
|
|
47
52
|
"format": "prettier --write \"{src,test,bin,scripts,examples}/**/*.{ts,mjs}\" package.json tsconfig.json tsconfig.check.json typedoc.json .oxlintrc.json examples/tsconfig.json \"examples/*/package.json\"",
|
|
48
53
|
"format:check": "prettier --check \"{src,test,bin,scripts,examples}/**/*.{ts,mjs}\" package.json tsconfig.json tsconfig.check.json typedoc.json .oxlintrc.json examples/tsconfig.json \"examples/*/package.json\"",
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// Markdown display (store proposal §5):
|
|
2
|
-
// import { MarkdownView, MarkdownBlock } from "@vestra/markdown.slint";
|
|
2
|
+
// import { MarkdownView, MarkdownBlocks, MarkdownBlock } from "@vestra/markdown.slint";
|
|
3
3
|
// Give `blocks` the value of `plugin.ui.markdown(text)`; the player parses
|
|
4
4
|
// the Markdown and fills the rows. Links report `link-clicked(url)` with the
|
|
5
5
|
// address resolved against the document's base; the plugin decides what to
|
|
6
6
|
// open (usually `plugin.system.openUrl`).
|
|
7
|
+
// `MarkdownView` scrolls one document. `MarkdownBlocks` does not scroll and
|
|
8
|
+
// is as tall as its content: put it in your own layout or in a `ListView`
|
|
9
|
+
// row (a chat message: a `[MarkdownBlock]` field of the row's struct).
|
|
10
|
+
// `code-copy: true` adds a 复制 button to code blocks that reports
|
|
11
|
+
// `copy-code(code)`; the plugin copies it (`plugin.clipboard`).
|
|
7
12
|
import { ScrollView } from "std-widgets.slint";
|
|
8
13
|
import { Theme } from "theme.slint";
|
|
9
14
|
import { Icon } from "icons.slint";
|
|
@@ -45,7 +50,9 @@ export struct MarkdownBlock {
|
|
|
45
50
|
component MarkdownBlockView {
|
|
46
51
|
in property <MarkdownBlock> block;
|
|
47
52
|
in property <length> available;
|
|
53
|
+
in property <bool> code-copy;
|
|
48
54
|
callback link-clicked(string);
|
|
55
|
+
callback copy-code(string);
|
|
49
56
|
private property <length> quote-indent: block.quote * 12px;
|
|
50
57
|
private property <length> list-indent: block.indent * 16px;
|
|
51
58
|
private property <length> inner: max(16px, root.available - root.quote-indent - root.list-indent);
|
|
@@ -124,6 +131,24 @@ component MarkdownBlockView {
|
|
|
124
131
|
default-font-size: Theme.font-body;
|
|
125
132
|
}
|
|
126
133
|
}
|
|
134
|
+
if root.code-copy: Rectangle {
|
|
135
|
+
x: parent.width - self.width - 4px;
|
|
136
|
+
y: 4px;
|
|
137
|
+
width: copy-label.preferred-width + 12px;
|
|
138
|
+
height: Theme.button-small-height;
|
|
139
|
+
border-radius: Theme.radius-control;
|
|
140
|
+
background: copy-touch.pressed ? Theme.button-press : copy-touch.has-hover ? Theme.button-hover : Theme.button-bg;
|
|
141
|
+
copy-label := Text {
|
|
142
|
+
text: "复制";
|
|
143
|
+
color: copy-touch.has-hover ? Theme.text : Theme.text-dim;
|
|
144
|
+
font-size: Theme.font-body;
|
|
145
|
+
vertical-alignment: center;
|
|
146
|
+
}
|
|
147
|
+
copy-touch := TouchArea {
|
|
148
|
+
mouse-cursor: pointer;
|
|
149
|
+
clicked => { root.copy-code(block.code); }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
127
152
|
}
|
|
128
153
|
if block.kind == "image" && block.has-image: HorizontalLayout {
|
|
129
154
|
alignment: start;
|
|
@@ -186,12 +211,38 @@ component MarkdownBlockView {
|
|
|
186
211
|
}
|
|
187
212
|
}
|
|
188
213
|
|
|
214
|
+
// Markdown blocks without scrolling, as tall as their content.
|
|
215
|
+
export component MarkdownBlocks inherits Rectangle {
|
|
216
|
+
in property <[MarkdownBlock]> blocks;
|
|
217
|
+
// A 复制 button on code blocks, reporting `copy-code(code)`.
|
|
218
|
+
in property <bool> code-copy: false;
|
|
219
|
+
callback link-clicked(string);
|
|
220
|
+
callback copy-code(string);
|
|
221
|
+
min-height: column.min-height;
|
|
222
|
+
preferred-height: column.preferred-height;
|
|
223
|
+
column := VerticalLayout {
|
|
224
|
+
width: root.width;
|
|
225
|
+
spacing: 8px;
|
|
226
|
+
alignment: start;
|
|
227
|
+
for block in root.blocks: MarkdownBlockView {
|
|
228
|
+
block: block;
|
|
229
|
+
available: root.width;
|
|
230
|
+
code-copy: root.code-copy;
|
|
231
|
+
link-clicked(url) => { root.link-clicked(url); }
|
|
232
|
+
copy-code(code) => { root.copy-code(code); }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
189
237
|
// A scrolling Markdown document.
|
|
190
238
|
export component MarkdownView inherits Rectangle {
|
|
191
239
|
in property <[MarkdownBlock]> blocks;
|
|
192
240
|
// Space around the document.
|
|
193
241
|
in property <length> inset: 10px;
|
|
242
|
+
// A 复制 button on code blocks, reporting `copy-code(code)`.
|
|
243
|
+
in property <bool> code-copy: false;
|
|
194
244
|
callback link-clicked(string);
|
|
245
|
+
callback copy-code(string);
|
|
195
246
|
scroll := ScrollView {
|
|
196
247
|
width: 100%;
|
|
197
248
|
height: 100%;
|
|
@@ -200,12 +251,12 @@ export component MarkdownView inherits Rectangle {
|
|
|
200
251
|
column := VerticalLayout {
|
|
201
252
|
width: scroll.visible-width;
|
|
202
253
|
padding: root.inset;
|
|
203
|
-
spacing: 8px;
|
|
204
254
|
alignment: start;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
255
|
+
MarkdownBlocks {
|
|
256
|
+
blocks: root.blocks;
|
|
257
|
+
code-copy: root.code-copy;
|
|
208
258
|
link-clicked(url) => { root.link-clicked(url); }
|
|
259
|
+
copy-code(code) => { root.copy-code(code); }
|
|
209
260
|
}
|
|
210
261
|
}
|
|
211
262
|
}
|