@vestra-player/sdk 1.5.3 → 1.5.4
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 +10 -0
- package/dist/plugin/testing.d.ts +9 -2
- package/dist/plugin/testing.js +9 -2
- package/dist/plugin/types.d.ts +53 -16
- package/dist/plugin/ui.js +22 -7
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +56 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.4
|
|
4
|
+
|
|
5
|
+
**Markdown in list rows** (the player after 0.4.0). Additive; `apiVersion` stays `vestra/1`, and plugins written for 1.5.3 need no change.
|
|
6
|
+
|
|
7
|
+
- **A `MarkdownRef` at any depth.** A document from `plugin.ui.markdown` may now be a field at any depth of a property value, wherever the Slint type is `[MarkdownBlock]`: for example the `body` of each row of a chat's `[Message]` list. Before, it could only be the whole value of a `[MarkdownBlock]` property. `PropValue` is now `PropData | ImageRef`, where the new **`PropData`** is JSON whose values may also be `MarkdownRef`s, at any depth. A released document is refused with `invalid` wherever it is in the value. An `ImageRef` stays a whole value only: nested in an array or object it is `invalid` (a type error in TypeScript, and refused by the SDK and the player). Older players refuse a nested document like any value they cannot convert.
|
|
8
|
+
- **Limits:** a document still holds at most 256 KB of text, 4000 blocks and 64 images; all of a value's text, its documents' text included, counts toward the value's 1 MiB; each document is one of its 4096 values.
|
|
9
|
+
- **`MarkdownBlocks`** (new in `@vestra/markdown.slint`): the blocks of a document without scrolling, as tall as their content, for your own layout or a `ListView` row. `in [MarkdownBlock] blocks`, `in bool code-copy` (default `false`), callbacks `link-clicked(string)` and `copy-code(string)`. `MarkdownView` is now a `ScrollView` around it and gains the same `code-copy` and `copy-code`; it looks as before.
|
|
10
|
+
- **Code copy, opt in.** With `code-copy: true`, code blocks get a 复制 button that reports `copy-code(code)` with the block's text; the plugin copies it (`plugin.clipboard.writeText`). Off by default: views that do not set it look and behave as before.
|
|
11
|
+
- **Cheaper updates** (player): writing a value again with unchanged documents does not parse them again (a new reference for the same text included), and a value with documents is updated in place where its shape allows: changed rows, rows added or removed at the end, and only the changed blocks of a changed document. Streaming a reply into the last row of a 200-message list therefore keeps the list, its scroll position and the other rows. Nothing to change in plugins.
|
|
12
|
+
- `TestHost` keeps nested documents in the view's properties as they were sent; `host.markdown(value)` reads one back from a field (`host.markdown(rows[0].body)`).
|
|
3
13
|
## 1.5.3
|
|
4
14
|
|
|
5
15
|
**File resolvers: local files as generated streams** (player 0.4.1). Additive; `apiVersion` stays `vestra/1`. Plugins that register one set `"minPlayerVersion": "0.4.1"`: an older player refuses the declaration (`invalid`).
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -1128,9 +1128,16 @@ export declare class TestHost implements Transport {
|
|
|
1128
1128
|
*/
|
|
1129
1129
|
readonly remoteImages: Map<string, string | null>;
|
|
1130
1130
|
/**
|
|
1131
|
-
* The Markdown document a
|
|
1131
|
+
* The Markdown document a value holds (its wire form read back), or
|
|
1132
1132
|
* `null` when the value is not one. `images` maps each remote address to
|
|
1133
|
-
* the downloaded file.
|
|
1133
|
+
* the downloaded file. Pass a whole property value or a field of one: a
|
|
1134
|
+
* document nested in rows is read the same way.
|
|
1135
|
+
*
|
|
1136
|
+
* @example
|
|
1137
|
+
* ```ts
|
|
1138
|
+
* const rows = host.window("chat").props["messages"] as { body: Json }[];
|
|
1139
|
+
* assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
|
|
1140
|
+
* ```
|
|
1134
1141
|
*/
|
|
1135
1142
|
markdown(value: Json | undefined): MarkdownWire | null;
|
|
1136
1143
|
/**
|
package/dist/plugin/testing.js
CHANGED
|
@@ -1482,9 +1482,16 @@ export class TestHost {
|
|
|
1482
1482
|
*/
|
|
1483
1483
|
remoteImages = new Map();
|
|
1484
1484
|
/**
|
|
1485
|
-
* The Markdown document a
|
|
1485
|
+
* The Markdown document a value holds (its wire form read back), or
|
|
1486
1486
|
* `null` when the value is not one. `images` maps each remote address to
|
|
1487
|
-
* the downloaded file.
|
|
1487
|
+
* the downloaded file. Pass a whole property value or a field of one: a
|
|
1488
|
+
* document nested in rows is read the same way.
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```ts
|
|
1492
|
+
* const rows = host.window("chat").props["messages"] as { body: Json }[];
|
|
1493
|
+
* assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
|
|
1494
|
+
* ```
|
|
1488
1495
|
*/
|
|
1489
1496
|
markdown(value) {
|
|
1490
1497
|
const doc = value
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -562,18 +562,31 @@ export interface ImageRef {
|
|
|
562
562
|
readonly __vestraImage: true;
|
|
563
563
|
}
|
|
564
564
|
/**
|
|
565
|
-
* A Markdown document for `MarkdownView`, made by
|
|
565
|
+
* A Markdown document for `MarkdownView` or `MarkdownBlocks`, made by
|
|
566
|
+
* {@link Ui.markdown}.
|
|
566
567
|
*
|
|
567
568
|
* @remarks
|
|
568
|
-
* Opaque, like {@link ImageRef}: pass it
|
|
569
|
-
* `[MarkdownBlock]
|
|
570
|
-
* `@vestra/markdown.slint`)
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
569
|
+
* Opaque, like {@link ImageRef}: pass it where the Slint type is
|
|
570
|
+
* `[MarkdownBlock]`: the whole value of such a property (the `blocks` of
|
|
571
|
+
* `MarkdownView` from `@vestra/markdown.slint`) or, from SDK 1.5.4, a
|
|
572
|
+
* `[MarkdownBlock]` field at any depth of a property value, such as the
|
|
573
|
+
* `body` of each row of a chat's message list. It carries the text, the
|
|
574
|
+
* base address and the local copies of its remote images; the player parses
|
|
575
|
+
* and draws it. Writing a document whose text did not change (a new
|
|
576
|
+
* reference for the same text included) costs the player no parsing, and a
|
|
577
|
+
* changed document updates only the blocks that changed. It belongs to the
|
|
578
|
+
* scope that created it: once that scope is disposed, writing it fails with
|
|
579
|
+
* `invalid`, wherever it is in the value.
|
|
580
|
+
*
|
|
581
|
+
* @example Chat messages, one document per row
|
|
582
|
+
* ```ts
|
|
583
|
+
* const chat = plugin.windows.create("ui/chat.slint");
|
|
584
|
+
* const body = await plugin.ui.markdown("**Hello**", { remoteImages: false });
|
|
585
|
+
* chat.set({ messages: [{ id: "m1", role: "assistant", body }] });
|
|
586
|
+
* ```
|
|
587
|
+
*
|
|
588
|
+
* @player 0.2.0; nested in a value: the player after 0.4.0
|
|
589
|
+
* @see {@link Ui.markdown}, {@link MarkdownOptions}, {@link PropValue}
|
|
577
590
|
* @since 1.1.0
|
|
578
591
|
* @category UI
|
|
579
592
|
*/
|
|
@@ -607,8 +620,9 @@ export interface MarkdownOptions {
|
|
|
607
620
|
remoteImages?: boolean;
|
|
608
621
|
}
|
|
609
622
|
/**
|
|
610
|
-
* The value of one Slint property: JSON
|
|
611
|
-
* as the whole value of
|
|
623
|
+
* The value of one Slint property: JSON with Markdown documents at any
|
|
624
|
+
* depth ({@link PropData}), or an image as the whole value of an `image`
|
|
625
|
+
* property.
|
|
612
626
|
*
|
|
613
627
|
* @remarks
|
|
614
628
|
* JSON becomes Slint values as you would expect: booleans for `bool`,
|
|
@@ -618,14 +632,37 @@ export interface MarkdownOptions {
|
|
|
618
632
|
* is refused (Slint treats `_` and `-` as the same, so use camelCase or
|
|
619
633
|
* kebab-case).
|
|
620
634
|
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
635
|
+
* A {@link MarkdownRef} goes where the Slint type is `[MarkdownBlock]`: the
|
|
636
|
+
* whole value, or a field at any depth (SDK 1.5.4; older players refuse a
|
|
637
|
+
* nested one). An {@link ImageRef} only as the whole value: nested in an
|
|
638
|
+
* array or object it is `invalid`.
|
|
639
|
+
*
|
|
640
|
+
* Limits per value: nesting depth 8, 4096 values in total (a document is
|
|
641
|
+
* one), 1024 rows per array, 64 KiB per string and 1 MiB of text in total,
|
|
642
|
+
* the text of its Markdown documents included (256 KB per document). A
|
|
643
|
+
* value over a limit is a failed write (see {@link View}).
|
|
624
644
|
*
|
|
625
645
|
* @since 1.0.0
|
|
626
646
|
* @category UI
|
|
627
647
|
*/
|
|
628
|
-
export type PropValue =
|
|
648
|
+
export type PropValue = PropData | ImageRef;
|
|
649
|
+
/**
|
|
650
|
+
* JSON whose values may also be Markdown documents, at any depth: the part
|
|
651
|
+
* of {@link PropValue} that nests.
|
|
652
|
+
*
|
|
653
|
+
* @example A message list whose rows carry a document
|
|
654
|
+
* ```ts
|
|
655
|
+
* const messages: PropData = [
|
|
656
|
+
* { id: "m1", role: "user", body: await plugin.ui.markdown("Hi") },
|
|
657
|
+
* ];
|
|
658
|
+
* ```
|
|
659
|
+
*
|
|
660
|
+
* @since 1.5.4
|
|
661
|
+
* @category UI
|
|
662
|
+
*/
|
|
663
|
+
export type PropData = null | boolean | number | string | MarkdownRef | PropData[] | {
|
|
664
|
+
[key: string]: PropData;
|
|
665
|
+
};
|
|
629
666
|
/**
|
|
630
667
|
* Properties to write to a view, by name.
|
|
631
668
|
*
|
package/dist/plugin/ui.js
CHANGED
|
@@ -31,18 +31,33 @@ export class ImageHandle {
|
|
|
31
31
|
return { $vestraImage: this.path, ref: this.#ref };
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* A property value as sent: images and documents in wire form, documents at
|
|
36
|
+
* any depth (released ones refused wherever they are); an image only as the
|
|
37
|
+
* whole value, like the player.
|
|
38
|
+
*/
|
|
39
|
+
function wireValue(value, nested = false) {
|
|
36
40
|
if (value instanceof MarkdownHandle) {
|
|
37
41
|
if (value.released)
|
|
38
42
|
throw failure("invalid", "Markdown document was released with the scope that created it");
|
|
39
43
|
return value.toJSON();
|
|
40
44
|
}
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (value instanceof ImageHandle) {
|
|
46
|
+
if (nested)
|
|
47
|
+
throw failure("invalid", `image ${value.path}: an image reference must be the whole value of an image property`);
|
|
48
|
+
if (value.released)
|
|
49
|
+
throw failure("invalid", `image ${value.path} was released with the scope that created it`);
|
|
50
|
+
return value.toJSON();
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return value.map((item) => wireValue(item, true));
|
|
54
|
+
if (value !== null && typeof value === "object") {
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, item] of Object.entries(value))
|
|
57
|
+
out[key] = wireValue(item, true);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
46
61
|
}
|
|
47
62
|
export const ui = (scope) => ({
|
|
48
63
|
image(path) {
|
package/package.json
CHANGED
|
@@ -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
|
}
|