@elicdavis/node-flow 0.1.15 → 0.1.17
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/.mocharc.json +5 -0
- package/dist/index.d.mts +139 -4
- package/dist/index.d.ts +139 -4
- package/dist/index.js +3754 -3352
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3741 -3352
- package/dist/index.mjs.map +1 -1
- package/index.html +0 -30
- package/package.json +4 -2
- package/src/graph.ts +234 -41
- package/src/index.ts +82 -40
- package/src/input.ts +22 -14
- package/src/markdown/entry.ts +26 -3
- package/src/markdown/lexicalParser.ts +22 -32
- package/src/markdown/syntaxParser.ts +93 -68
- package/src/minimap.ts +178 -0
- package/src/node.ts +114 -0
- package/src/nodes/factory.ts +9 -5
- package/src/nodes/publisher.ts +39 -4
- package/src/nodes/subsystem.ts +218 -8
- package/src/notes/subsystem.ts +3 -0
- package/src/organize.ts +276 -181
- package/src/port.ts +143 -7
- package/src/render_scheduler.ts +49 -0
- package/src/styles/text.ts +8 -0
- package/src/theme.ts +8 -0
- package/src/types/text.ts +8 -4
- package/src/utils/string.ts +7 -1
- package/src/widgets/color.ts +5 -6
- package/src/widgets/factory.ts +1 -1
- package/src/widgets/image.ts +2 -0
- package/src/widgets/number.ts +2 -2
- package/src/widgets/slider.ts +2 -2
- package/src/widgets/string.ts +2 -2
- package/src/widgets/toggle.ts +2 -2
- package/src/widgets/widget.ts +22 -0
- package/test/markdown.test.ts +165 -0
- package/test/minimap.test.ts +103 -0
- package/test/node_menu_filter.test.ts +119 -0
- package/test/organize.test.ts +156 -0
- package/test/port.test.ts +125 -0
- package/test/render_scheduler.test.ts +119 -0
- package/tsconfig.json +6 -2
package/src/port.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { Connection } from './connection';
|
|
3
|
+
import { requestRender } from "./render_scheduler";
|
|
3
4
|
import { RenderElementBase } from "./elements/base";
|
|
4
5
|
import { ContainerRenderElement, AlignItems } from './elements/container';
|
|
5
6
|
import { TextAlign, TextElement } from "./elements/text";
|
|
@@ -31,12 +32,62 @@ export interface PortConfig {
|
|
|
31
32
|
type?: string;
|
|
32
33
|
description?: string;
|
|
33
34
|
array?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lets this port connect to a port of any type, instead of only to one
|
|
38
|
+
* declaring the same `type` string. For a port whose type is decided by
|
|
39
|
+
* whatever gets wired into it, where `type` is a placeholder shown to
|
|
40
|
+
* the user rather than a promise about the values passing through.
|
|
41
|
+
*/
|
|
42
|
+
anyType?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every type this port takes, when it takes more than one. For a port
|
|
46
|
+
* that works on a single value or an array of them, where `type` is
|
|
47
|
+
* only the one it happens to be showing.
|
|
48
|
+
*
|
|
49
|
+
* An empty or absent list means the port takes exactly its `type`,
|
|
50
|
+
* unless `anyType` says otherwise.
|
|
51
|
+
*/
|
|
52
|
+
acceptedTypes?: Array<string>;
|
|
53
|
+
|
|
34
54
|
emptyStyle?: PortStyle;
|
|
35
55
|
filledStyle?: PortStyle;
|
|
36
56
|
onConnectionAdded?: ConnectionChangeCallback;
|
|
37
57
|
onConnectionRemoved?: ConnectionChangeCallback;
|
|
38
58
|
};
|
|
39
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Whether two ports may be connected, going by their types alone. Each port
|
|
62
|
+
* offers the set of types it takes - one entry for an ordinary port, several
|
|
63
|
+
* for a port that takes more than one, and none at all for a port that takes
|
|
64
|
+
* anything. They connect when those sets overlap.
|
|
65
|
+
*/
|
|
66
|
+
export function portsCompatible(a: Port | undefined | null, b: Port | undefined | null): boolean {
|
|
67
|
+
if (!a || !b) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return typeSetsCompatible(a.acceptedTypes(), b.acceptedTypes());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function typeSetsCompatible(a: Array<string>, b: Array<string>): boolean {
|
|
75
|
+
if (a.length === 0 || b.length === 0) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return a.some((type) => b.includes(type));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function portConfigTypes(config: PortConfig): Array<string> {
|
|
82
|
+
if (config.anyType === true) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (config.acceptedTypes !== undefined && config.acceptedTypes.length > 0) {
|
|
86
|
+
return config.acceptedTypes;
|
|
87
|
+
}
|
|
88
|
+
return [config.type === undefined ? "" : config.type];
|
|
89
|
+
}
|
|
90
|
+
|
|
40
91
|
// Calculate a color hash for an arbirary type
|
|
41
92
|
function fallbackColor(type: string, s: number): string {
|
|
42
93
|
let value = 0;
|
|
@@ -69,6 +120,18 @@ export class Port {
|
|
|
69
120
|
|
|
70
121
|
#dataType: string;
|
|
71
122
|
|
|
123
|
+
#anyType: boolean;
|
|
124
|
+
|
|
125
|
+
#acceptedTypes: Array<string>;
|
|
126
|
+
|
|
127
|
+
#description: string;
|
|
128
|
+
|
|
129
|
+
#array: boolean;
|
|
130
|
+
|
|
131
|
+
#configuredEmptyFill: string | undefined;
|
|
132
|
+
|
|
133
|
+
#configuredFilledFill: string | undefined;
|
|
134
|
+
|
|
72
135
|
#dataTypePopupElement: RenderElementBase;
|
|
73
136
|
|
|
74
137
|
#onConnectionAdded: Array<ConnectionChangeCallback>;
|
|
@@ -81,17 +144,23 @@ export class Port {
|
|
|
81
144
|
this.#portType = portType;
|
|
82
145
|
this.#displayName = config?.name === undefined ? "Port" : config?.name;
|
|
83
146
|
this.#dataType = config?.type === undefined ? "" : config?.type;
|
|
147
|
+
this.#anyType = config?.anyType === true;
|
|
148
|
+
this.#acceptedTypes = config?.acceptedTypes === undefined ? [] : config.acceptedTypes;
|
|
149
|
+
this.#description = config?.description === undefined ? "" : config.description;
|
|
150
|
+
this.#array = config?.array === true;
|
|
151
|
+
this.#configuredEmptyFill = config?.emptyStyle?.fillColor;
|
|
152
|
+
this.#configuredFilledFill = config?.filledStyle?.fillColor;
|
|
84
153
|
|
|
85
154
|
this.#emptyStyle = {
|
|
86
155
|
borderColor: config?.emptyStyle?.borderColor === undefined ? "#1c1c1c" : config.emptyStyle?.borderColor,
|
|
87
|
-
fillColor:
|
|
156
|
+
fillColor: this.#configuredEmptyFill === undefined ? fallbackColor(this.#dataType, 0.3) : this.#configuredEmptyFill,
|
|
88
157
|
borderSize: config?.emptyStyle?.borderSize === undefined ? 1 : config.emptyStyle?.borderSize,
|
|
89
158
|
size: config?.emptyStyle?.size === undefined ? 4 : config.emptyStyle?.size
|
|
90
159
|
}
|
|
91
160
|
|
|
92
161
|
this.#filledStyle = {
|
|
93
162
|
borderColor: config?.filledStyle?.borderColor === undefined ? "#1c1c1c" : config.filledStyle?.borderColor,
|
|
94
|
-
fillColor:
|
|
163
|
+
fillColor: this.#configuredFilledFill === undefined ? fallbackColor(this.#dataType, 1) : this.#configuredFilledFill,
|
|
95
164
|
borderSize: config?.filledStyle?.borderSize === undefined ? 1 : config.filledStyle?.borderSize,
|
|
96
165
|
size: config?.filledStyle?.size === undefined ? 5 : config.filledStyle?.size
|
|
97
166
|
}
|
|
@@ -106,9 +175,20 @@ export class Port {
|
|
|
106
175
|
this.#onConnectionRemoved.push(config?.onConnectionRemoved);
|
|
107
176
|
}
|
|
108
177
|
|
|
178
|
+
this.#dataTypePopupElement = this.#buildTooltip();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#buildTooltip(): RenderElementBase {
|
|
109
182
|
const containerElements = new Array<RenderElementBase>();
|
|
183
|
+
|
|
110
184
|
let dataTypeDisplay = this.#dataType;
|
|
111
|
-
if (
|
|
185
|
+
if (this.#acceptedTypes.length > 1) {
|
|
186
|
+
dataTypeDisplay = this.#acceptedTypes.join(" or ");
|
|
187
|
+
}
|
|
188
|
+
if (dataTypeDisplay === "" && this.#anyType) {
|
|
189
|
+
dataTypeDisplay = "any";
|
|
190
|
+
}
|
|
191
|
+
if (this.#array) {
|
|
112
192
|
dataTypeDisplay = "Array<" + dataTypeDisplay + ">"
|
|
113
193
|
}
|
|
114
194
|
containerElements.push(new TextElement(
|
|
@@ -120,10 +200,9 @@ export class Port {
|
|
|
120
200
|
}
|
|
121
201
|
));
|
|
122
202
|
|
|
123
|
-
|
|
124
|
-
if (description && description !== "") {
|
|
203
|
+
if (this.#description !== "") {
|
|
125
204
|
containerElements.push(new TextElement(
|
|
126
|
-
new Text(description, { color: "white", style: FontStyle.Italic }),
|
|
205
|
+
new Text(this.#description, { color: "white", style: FontStyle.Italic }),
|
|
127
206
|
{
|
|
128
207
|
Align: TextAlign.Center,
|
|
129
208
|
Padding: { Top: 16 },
|
|
@@ -133,7 +212,7 @@ export class Port {
|
|
|
133
212
|
));
|
|
134
213
|
}
|
|
135
214
|
|
|
136
|
-
|
|
215
|
+
return new ContainerRenderElement(
|
|
137
216
|
containerElements,
|
|
138
217
|
{
|
|
139
218
|
BackgroundColor: "rgba(0, 0, 0, 0.85)",
|
|
@@ -145,6 +224,42 @@ export class Port {
|
|
|
145
224
|
);
|
|
146
225
|
}
|
|
147
226
|
|
|
227
|
+
#refreshDataType(): void {
|
|
228
|
+
if (this.#configuredEmptyFill === undefined) {
|
|
229
|
+
this.#emptyStyle.fillColor = fallbackColor(this.#dataType, 0.3);
|
|
230
|
+
}
|
|
231
|
+
if (this.#configuredFilledFill === undefined) {
|
|
232
|
+
this.#filledStyle.fillColor = fallbackColor(this.#dataType, 1);
|
|
233
|
+
}
|
|
234
|
+
this.#dataTypePopupElement = this.#buildTooltip();
|
|
235
|
+
requestRender();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
setDataType(type: string): void {
|
|
239
|
+
if (this.#dataType === type) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.#dataType = type;
|
|
243
|
+
this.#refreshDataType();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
setAnyType(anyType: boolean): void {
|
|
247
|
+
if (this.#anyType === anyType) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.#anyType = anyType;
|
|
251
|
+
this.#refreshDataType();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Scope accepted types to singular type
|
|
256
|
+
*/
|
|
257
|
+
settleOn(type: string): void {
|
|
258
|
+
this.#acceptedTypes = [type];
|
|
259
|
+
this.setDataType(type);
|
|
260
|
+
this.#refreshDataType();
|
|
261
|
+
}
|
|
262
|
+
|
|
148
263
|
addConnection(connection: Connection): void {
|
|
149
264
|
const c = this.#connections.length;
|
|
150
265
|
this.#connections.push(connection);
|
|
@@ -195,6 +310,27 @@ export class Port {
|
|
|
195
310
|
return this.#dataType;
|
|
196
311
|
}
|
|
197
312
|
|
|
313
|
+
acceptsAnyType(): boolean {
|
|
314
|
+
return this.#anyType;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Every type this port takes. Empty means it takes anything.
|
|
319
|
+
*/
|
|
320
|
+
acceptedTypes(): Array<string> {
|
|
321
|
+
if (this.#anyType) {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
if (this.#acceptedTypes.length > 0) {
|
|
325
|
+
return this.#acceptedTypes;
|
|
326
|
+
}
|
|
327
|
+
return [this.#dataType];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
setAcceptedTypes(types: Array<string>): void {
|
|
331
|
+
this.#acceptedTypes = types;
|
|
332
|
+
}
|
|
333
|
+
|
|
198
334
|
getPortType(): PortType {
|
|
199
335
|
return this.#portType;
|
|
200
336
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
type Redraw = () => void;
|
|
2
|
+
|
|
3
|
+
const subscribers = new Set<Redraw>();
|
|
4
|
+
let frameQueued = false;
|
|
5
|
+
let continuous = 0;
|
|
6
|
+
|
|
7
|
+
const canSchedule = (): boolean =>
|
|
8
|
+
typeof requestAnimationFrame === "function";
|
|
9
|
+
|
|
10
|
+
export function onRenderRequested(redraw: Redraw): () => void {
|
|
11
|
+
subscribers.add(redraw);
|
|
12
|
+
return () => {
|
|
13
|
+
subscribers.delete(redraw);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export function requestRender(): void {
|
|
19
|
+
if (frameQueued || !canSchedule()) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
frameQueued = true;
|
|
24
|
+
requestAnimationFrame(() => {
|
|
25
|
+
frameQueued = false;
|
|
26
|
+
subscribers.forEach((redraw) => redraw());
|
|
27
|
+
|
|
28
|
+
// Something asked to be redrawn every frame, so keep the loop
|
|
29
|
+
// alive by asking again on its behalf.
|
|
30
|
+
if (continuous > 0) {
|
|
31
|
+
requestRender();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export function renderContinuously(): () => void {
|
|
38
|
+
continuous++;
|
|
39
|
+
requestRender();
|
|
40
|
+
|
|
41
|
+
let released = false;
|
|
42
|
+
return () => {
|
|
43
|
+
if (released) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
released = true;
|
|
47
|
+
continuous--;
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/styles/text.ts
CHANGED
|
@@ -65,6 +65,14 @@ export class TextStyle {
|
|
|
65
65
|
return this.#font;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
getWeight(): FontWeight {
|
|
69
|
+
return this.#weight;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getStyle(): FontStyle {
|
|
73
|
+
return this.#fontStyle;
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
measure(ctx: CanvasRenderingContext2D, scale: number, text: string, out: Vector2): void {
|
|
69
77
|
this.setupStyle(ctx, scale);
|
|
70
78
|
const measurements = ctx.measureText(text)
|
package/src/theme.ts
CHANGED
|
@@ -59,6 +59,14 @@ export const Theme = {
|
|
|
59
59
|
FontColor: "#afb9bb",
|
|
60
60
|
DisabledFontColor: "#4a585c",
|
|
61
61
|
},
|
|
62
|
+
Minimap: {
|
|
63
|
+
BackgroundColor: "rgba(7, 33, 42, 0.85)",
|
|
64
|
+
BorderColor: "#1c1c1c",
|
|
65
|
+
BorderRadius: 4,
|
|
66
|
+
Padding: 6,
|
|
67
|
+
NodeColor: "#6b7f85",
|
|
68
|
+
ViewportColor: "rgba(175, 185, 187, 0.25)",
|
|
69
|
+
},
|
|
62
70
|
Note: {
|
|
63
71
|
FontColor: "#afb9bb",
|
|
64
72
|
FontSize: 16,
|
package/src/types/text.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requestRender } from "../render_scheduler";
|
|
1
2
|
import { FontWeight, TextStyle, TextStyleConfig } from "../styles/text";
|
|
2
3
|
import { splitString, splitStringIntoLines } from "../utils/string";
|
|
3
4
|
import { CopyVector2, ScaleVector, Vector2, Zero } from "./vector2";
|
|
@@ -32,10 +33,13 @@ export class Text {
|
|
|
32
33
|
this.#lineSpacing = config?.LineSpacing ? config?.LineSpacing : 5;
|
|
33
34
|
this.#textToRender = [value];
|
|
34
35
|
|
|
35
|
-
if (
|
|
36
|
-
document.fonts.
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (typeof document !== "undefined" && document.fonts !== undefined) {
|
|
37
|
+
if (!document.fonts.check(`16px "${this.#style.getFont()}"`)) {
|
|
38
|
+
document.fonts.addEventListener("loadingdone", () => {
|
|
39
|
+
this.#measured = false;
|
|
40
|
+
requestRender();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
package/src/utils/string.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
interface BinarySearch {
|
|
2
|
+
max: number;
|
|
3
|
+
getValue: (guess: number) => number;
|
|
4
|
+
match: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const binarySearch = ({ max, getValue, match }: BinarySearch): number => {
|
|
2
8
|
let min = 0;
|
|
3
9
|
|
|
4
10
|
while (min <= max) {
|
package/src/widgets/color.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { HexToColor } from "../utils/color";
|
|
|
2
2
|
import { TextStyleConfig } from "../styles/text";
|
|
3
3
|
import { Box } from "../types/box";
|
|
4
4
|
import { CopyVector2, Vector2 } from "../types/vector2";
|
|
5
|
-
import { height, width } from "./widget";
|
|
5
|
+
import { height, startingWidgetValue, width } from "./widget";
|
|
6
6
|
import { Popup } from "../popup";
|
|
7
7
|
import { TextBoxStyle } from '../styles/textBox';
|
|
8
8
|
import { Theme } from "../theme";
|
|
@@ -46,12 +46,12 @@ export class ColorWidget {
|
|
|
46
46
|
#callback?: (newColor: string) => void;
|
|
47
47
|
|
|
48
48
|
constructor(node: FlowNode, config?: ColorWidgetConfig) {
|
|
49
|
-
this.#value = "#000000"
|
|
50
|
-
this.#contrast = contrastColor(this.#value);
|
|
51
|
-
this.Set(config?.value === undefined ? "#000000" : config?.value);
|
|
52
|
-
|
|
53
49
|
this.#node = node;
|
|
54
50
|
this.#nodeProperty = config?.property;
|
|
51
|
+
this.#callback = config?.callback;
|
|
52
|
+
|
|
53
|
+
this.#value = startingWidgetValue(node, this.#nodeProperty, config?.value, "#000000");
|
|
54
|
+
this.#contrast = contrastColor(this.#value);
|
|
55
55
|
this.#textBoxStyle = new TextBoxStyle({
|
|
56
56
|
box: {
|
|
57
57
|
color: this.#value,
|
|
@@ -62,7 +62,6 @@ export class ColorWidget {
|
|
|
62
62
|
},
|
|
63
63
|
text: config?.textStyle
|
|
64
64
|
});
|
|
65
|
-
this.#callback = config?.callback;
|
|
66
65
|
|
|
67
66
|
if (this.#nodeProperty !== undefined) {
|
|
68
67
|
this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
|
package/src/widgets/factory.ts
CHANGED
package/src/widgets/image.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requestRender } from "../render_scheduler";
|
|
1
2
|
import { Box } from "../types/box";
|
|
2
3
|
import { CopyVector2, Vector2 } from "../types/vector2";
|
|
3
4
|
|
|
@@ -101,6 +102,7 @@ export class ImageWidget {
|
|
|
101
102
|
img.src = url;
|
|
102
103
|
img.onload = () => {
|
|
103
104
|
this.#image = img;
|
|
105
|
+
requestRender();
|
|
104
106
|
};
|
|
105
107
|
img.onerror = (event) => {
|
|
106
108
|
console.log("error loading image:", url, event);
|
package/src/widgets/number.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Popup } from "../popup";
|
|
|
3
3
|
import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../styles/textBox";
|
|
4
4
|
import { Box, InBox } from '../types/box';
|
|
5
5
|
import { CopyVector2, Vector2 } from "../types/vector2";
|
|
6
|
-
import { height, width } from "./widget";
|
|
6
|
+
import { height, startingWidgetValue, width } from "./widget";
|
|
7
7
|
import { FlowNode } from "../node";
|
|
8
8
|
|
|
9
9
|
export interface NumberWidgetConfig {
|
|
@@ -62,8 +62,8 @@ export class NumberWidget {
|
|
|
62
62
|
},
|
|
63
63
|
text: { color: Theme.Widget.FontColor },
|
|
64
64
|
}));
|
|
65
|
-
this.Set(config?.value === undefined ? 0 : config?.value);
|
|
66
65
|
this.#callback = config?.callback;
|
|
66
|
+
this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
|
|
67
67
|
|
|
68
68
|
if (this.#nodeProperty !== undefined) {
|
|
69
69
|
this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
|
package/src/widgets/slider.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { TextStyle, TextStyleConfig, TextStyleFallback } from "../styles/text";
|
|
|
4
4
|
import { Box } from '../types/box';
|
|
5
5
|
import { CopyVector2, Vector2, Zero } from "../types/vector2";
|
|
6
6
|
import { Clamp, Clamp01 } from "../utils/math";
|
|
7
|
-
import { height, width } from "./widget";
|
|
7
|
+
import { height, startingWidgetValue, width } from "./widget";
|
|
8
8
|
import { TextBaseline } from "../styles/canvasTextBaseline";
|
|
9
9
|
import { FlowNode } from "../node";
|
|
10
10
|
|
|
@@ -104,7 +104,7 @@ export class SliderWidget {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
this.SetValue(config?.value
|
|
107
|
+
this.SetValue(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
|
|
108
108
|
|
|
109
109
|
// Setup change callback after we set the initial value to prevent the callback from being
|
|
110
110
|
this.#change = config?.change;
|
package/src/widgets/string.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../s
|
|
|
3
3
|
import { Box, InBox } from "../types/box";
|
|
4
4
|
import { CopyVector2, Vector2 } from "../types/vector2";
|
|
5
5
|
import { fitString } from "../utils/string";
|
|
6
|
-
import { height, width } from "./widget";
|
|
6
|
+
import { height, startingWidgetValue, width } from "./widget";
|
|
7
7
|
import { FlowNode } from '../node';
|
|
8
8
|
import { SetStringPopup } from "../popups/string";
|
|
9
9
|
|
|
@@ -60,8 +60,8 @@ export class StringWidget {
|
|
|
60
60
|
text: { color: Theme.Widget.FontColor },
|
|
61
61
|
}));
|
|
62
62
|
|
|
63
|
-
this.Set(config?.value === undefined ? "" : config?.value);
|
|
64
63
|
this.#callback = config?.callback;
|
|
64
|
+
this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
|
|
65
65
|
if (this.#nodeProperty !== undefined) {
|
|
66
66
|
this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
|
|
67
67
|
this.Set(newVal);
|
package/src/widgets/toggle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Theme } from "../theme";
|
|
2
2
|
import { Box, InBox } from "../types/box";
|
|
3
3
|
import { CopyVector2, Vector2 } from "../types/vector2";
|
|
4
|
-
import { height, width } from "./widget";
|
|
4
|
+
import { height, startingWidgetValue, width } from "./widget";
|
|
5
5
|
import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../styles/textBox";
|
|
6
6
|
import { FlowNode } from "../node";
|
|
7
7
|
|
|
@@ -159,8 +159,8 @@ export class ToggleWidget {
|
|
|
159
159
|
lightColor: config?.disabledStyle?.lightColor === undefined ? "#004400" : config?.enabledStyle?.lightColor,
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
this.Set(config?.value === undefined ? false : config?.value);
|
|
163
162
|
this.#callback = config?.callback;
|
|
163
|
+
this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
|
|
164
164
|
if (this.#nodeProperty !== undefined) {
|
|
165
165
|
this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
|
|
166
166
|
this.Set(newVal);
|
package/src/widgets/widget.ts
CHANGED
|
@@ -9,4 +9,26 @@ export interface Widget {
|
|
|
9
9
|
Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box
|
|
10
10
|
ClickStart(): void
|
|
11
11
|
ClickEnd(): void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PropertyReader {
|
|
15
|
+
getProperty(name: string): any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function startingWidgetValue<T>(
|
|
19
|
+
node: PropertyReader,
|
|
20
|
+
property: string | undefined,
|
|
21
|
+
configured: T | undefined,
|
|
22
|
+
fallback: T,
|
|
23
|
+
): T {
|
|
24
|
+
if (configured !== undefined) {
|
|
25
|
+
return configured;
|
|
26
|
+
}
|
|
27
|
+
if (property !== undefined && property !== null) {
|
|
28
|
+
const existing = node.getProperty(property);
|
|
29
|
+
if (existing !== undefined && existing !== null) {
|
|
30
|
+
return existing as T;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return fallback;
|
|
12
34
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as assert from 'assert';
|
|
2
|
+
import { BuildMarkdown } from '../src/markdown/markdown';
|
|
3
|
+
import { BasicMarkdownEntry, CodeBlockEntry, UnorderedListMarkdownEntry } from '../src/markdown/entry';
|
|
4
|
+
import { FontStyle, FontWeight } from '../src/styles/text';
|
|
5
|
+
import { Text } from '../src/types/text';
|
|
6
|
+
|
|
7
|
+
/** The runs of text in an entry, as [content, weight, style] triples. */
|
|
8
|
+
function runs(entry: any): Array<[string, FontWeight, FontStyle]> {
|
|
9
|
+
let lines: ReadonlyArray<Text>;
|
|
10
|
+
if (entry.entries) {
|
|
11
|
+
lines = entry.entries().flatMap((item: any) => item.lines());
|
|
12
|
+
} else if (entry.lines) {
|
|
13
|
+
lines = entry.lines();
|
|
14
|
+
} else {
|
|
15
|
+
lines = [entry.text()];
|
|
16
|
+
}
|
|
17
|
+
return lines.map(line => [
|
|
18
|
+
line.get(),
|
|
19
|
+
line.style().getWeight(),
|
|
20
|
+
line.style().getStyle(),
|
|
21
|
+
] as [string, FontWeight, FontStyle]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function contentOf(entry: any): string {
|
|
25
|
+
return runs(entry).map(r => r[0]).join('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('markdown: paragraphs and headers', () => {
|
|
29
|
+
it('reads a plain line', () => {
|
|
30
|
+
const entries = BuildMarkdown('hello world');
|
|
31
|
+
assert.strictEqual(entries.length, 1);
|
|
32
|
+
assert.strictEqual(contentOf(entries[0]), 'hello world');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reads each header level', () => {
|
|
36
|
+
const entries = BuildMarkdown('# one\n## two\n### three');
|
|
37
|
+
assert.strictEqual(entries.length, 3);
|
|
38
|
+
assert.strictEqual(contentOf(entries[0]), 'one');
|
|
39
|
+
assert.strictEqual(contentOf(entries[1]), 'two');
|
|
40
|
+
assert.strictEqual(contentOf(entries[2]), 'three');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('keeps a # that is not at the start of a line as text', () => {
|
|
44
|
+
const entries = BuildMarkdown('issue #42 filed');
|
|
45
|
+
assert.strictEqual(contentOf(entries[0]), 'issue #42 filed');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('markdown: emphasis', () => {
|
|
50
|
+
it('italicises a single-starred run', () => {
|
|
51
|
+
const entries = BuildMarkdown('*hello*');
|
|
52
|
+
assert.deepStrictEqual(runs(entries[0]), [['hello', FontWeight.Normal, FontStyle.Italic]]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('bolds a double-starred run', () => {
|
|
56
|
+
const entries = BuildMarkdown('**hello**');
|
|
57
|
+
assert.deepStrictEqual(runs(entries[0]), [['hello', FontWeight.Bold, FontStyle.Normal]]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('emphasises a run in the middle of a line', () => {
|
|
61
|
+
const entries = BuildMarkdown('a *b* c');
|
|
62
|
+
assert.deepStrictEqual(runs(entries[0]), [
|
|
63
|
+
['a ', FontWeight.Normal, FontStyle.Normal],
|
|
64
|
+
['b', FontWeight.Normal, FontStyle.Italic],
|
|
65
|
+
[' c', FontWeight.Normal, FontStyle.Normal],
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('leaves a star surrounded by spaces alone', () => {
|
|
70
|
+
const entries = BuildMarkdown('2 * 3 * 4');
|
|
71
|
+
assert.strictEqual(contentOf(entries[0]), '2 * 3 * 4');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('keeps stars that never matched as text', () => {
|
|
75
|
+
assert.strictEqual(contentOf(BuildMarkdown('*unclosed')[0]), '*unclosed');
|
|
76
|
+
assert.strictEqual(contentOf(BuildMarkdown('a * b')[0]), 'a * b');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps the stars left over when the runs are uneven', () => {
|
|
80
|
+
// Two opened, one closed: the extra opener is just a character.
|
|
81
|
+
assert.deepStrictEqual(runs(BuildMarkdown('**a*')[0]), [
|
|
82
|
+
['*', FontWeight.Normal, FontStyle.Normal],
|
|
83
|
+
['a', FontWeight.Normal, FontStyle.Italic],
|
|
84
|
+
]);
|
|
85
|
+
assert.deepStrictEqual(runs(BuildMarkdown('*a**')[0]), [
|
|
86
|
+
['a', FontWeight.Normal, FontStyle.Italic],
|
|
87
|
+
['*', FontWeight.Normal, FontStyle.Normal],
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('does not emphasise across a space held against the stars', () => {
|
|
92
|
+
assert.strictEqual(contentOf(BuildMarkdown('** a **')[0]), '** a **');
|
|
93
|
+
assert.strictEqual(contentOf(BuildMarkdown('**a **')[0]), '**a **');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('emphasises a run holding a space in the middle', () => {
|
|
97
|
+
assert.deepStrictEqual(runs(BuildMarkdown('**a f**')[0]), [
|
|
98
|
+
['a f', FontWeight.Bold, FontStyle.Normal],
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('never produces an empty run', () => {
|
|
103
|
+
for (const source of ['*a*', '**a**', '*a**', '**a*', '* a *', 'a `b` c']) {
|
|
104
|
+
for (const [content] of runs(BuildMarkdown(source)[0])) {
|
|
105
|
+
assert.notStrictEqual(content, '', `${source} produced an empty run`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('emphasises more than one run on a line', () => {
|
|
111
|
+
const entries = BuildMarkdown('*a* and *b*');
|
|
112
|
+
assert.deepStrictEqual(runs(entries[0]), [
|
|
113
|
+
['a', FontWeight.Normal, FontStyle.Italic],
|
|
114
|
+
[' and ', FontWeight.Normal, FontStyle.Normal],
|
|
115
|
+
['b', FontWeight.Normal, FontStyle.Italic],
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('markdown: lists', () => {
|
|
121
|
+
it('collects consecutive star lines into one list', () => {
|
|
122
|
+
const entries = BuildMarkdown('* one\n* two\n* three');
|
|
123
|
+
assert.strictEqual(entries.length, 1);
|
|
124
|
+
assert.ok(entries[0] instanceof UnorderedListMarkdownEntry);
|
|
125
|
+
assert.strictEqual((entries[0] as any).entries().length, 3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('ends the list at the first line that is not one', () => {
|
|
129
|
+
const entries = BuildMarkdown('* one\n* two\nafter');
|
|
130
|
+
assert.strictEqual(entries.length, 2);
|
|
131
|
+
assert.ok(entries[0] instanceof UnorderedListMarkdownEntry);
|
|
132
|
+
assert.strictEqual(contentOf(entries[1]), 'after');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('markdown: code', () => {
|
|
137
|
+
it('sets inline code apart without keeping the backticks', () => {
|
|
138
|
+
const entry = BuildMarkdown('a `b` c')[0] as any;
|
|
139
|
+
assert.deepStrictEqual(runs(entry).map(r => r[0]), ['a ', 'b', ' c']);
|
|
140
|
+
assert.strictEqual(entry.lines()[1].style().getFont(), 'monospace');
|
|
141
|
+
assert.notStrictEqual(entry.lines()[0].style().getFont(), 'monospace');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('keeps a lone backtick as text', () => {
|
|
145
|
+
assert.strictEqual(contentOf(BuildMarkdown('a ` b')[0]), 'a ` b');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('keeps a fenced block verbatim', () => {
|
|
149
|
+
const entries = BuildMarkdown('```\nlet a = *b*;\n```');
|
|
150
|
+
assert.strictEqual(entries.length, 1);
|
|
151
|
+
assert.ok(entries[0] instanceof CodeBlockEntry, `got ${entries[0].constructor.name}`);
|
|
152
|
+
assert.strictEqual(contentOf(entries[0]), 'let a = *b*;\n');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('keeps more than one line of a fenced block', () => {
|
|
156
|
+
const entries = BuildMarkdown('```\none\ntwo\n```');
|
|
157
|
+
assert.strictEqual(contentOf(entries[0]), 'one\ntwo\n');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('carries on after a fenced block', () => {
|
|
161
|
+
const entries = BuildMarkdown('```\ncode\n```\nafter');
|
|
162
|
+
assert.strictEqual(entries.length, 2);
|
|
163
|
+
assert.strictEqual(contentOf(entries[1]), 'after');
|
|
164
|
+
});
|
|
165
|
+
});
|