@elicdavis/node-flow 0.1.14 → 0.1.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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@elicdavis/node-flow",
3
3
  "description": "build node graphs",
4
4
  "author": "Eli C Davis",
5
- "version": "0.1.14",
5
+ "version": "0.1.16",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
@@ -15,7 +15,9 @@
15
15
  "scripts": {
16
16
  "build": "ts-node ./esbuild.ts",
17
17
  "watch": "npm run build -- --watch",
18
- "package": "tsup"
18
+ "package": "tsup",
19
+ "test": "mocha",
20
+ "typecheck": "tsc --noEmit"
19
21
  },
20
22
  "homepage": "https://github.com/EliCDavis/node-flow",
21
23
  "repository": {
package/src/graph.ts CHANGED
@@ -403,6 +403,13 @@ export class NodeFlowGraph {
403
403
  this.#camera.setOrientation(orientation);
404
404
  }
405
405
 
406
+ /**
407
+ * Resets the camera to the default orientation (origin, zoom 1).
408
+ */
409
+ resetCamera(): void {
410
+ this.#camera.reset();
411
+ }
412
+
406
413
  /**
407
414
  * Adjusts camera position and zoom so all nodes fit in view (~10% padding).
408
415
  * Falls back to camera.reset() when there are no nodes.
package/src/index.ts CHANGED
@@ -1,40 +1,66 @@
1
- export * from './graph';
2
- export * from './node';
3
-
4
- import { ContextMenuConfig, ContextMenuItemConfig, ContextMenuItemState } from './contextMenu';
5
- export { ContextMenuConfig, ContextMenuItemConfig, ContextMenuItemState };
6
-
7
- import { Theme } from './theme';
8
- export { Theme };
9
-
10
- import { Publisher, PublisherConfig } from './nodes/publisher';
11
- export { Publisher, PublisherConfig };
12
-
13
- import { FlowNote, FlowNoteConfig, NoteContentChangeCallback, NoteWidthChangeCallback } from './notes/note';
14
- export { FlowNote, FlowNoteConfig, NoteContentChangeCallback, NoteWidthChangeCallback };
15
-
16
- import {
17
- NoteAddedCallback, NoteDragStartCallback, NoteDragStopCallback,
18
- NoteRemovedCallback, NoteSubsystemConfig
19
- } from './notes/subsystem';
20
- export {
21
- NoteAddedCallback, NoteDragStartCallback, NoteDragStopCallback,
22
- NoteRemovedCallback, NoteSubsystemConfig
23
- };
24
-
25
- // Widgets
26
- import { NumberWidget } from './widgets/number';
27
- import { ColorWidget } from './widgets/color';
28
- import { StringWidget } from './widgets/string';
29
- import { TextWidget } from './widgets/text';
30
- import { ButtonWidget } from './widgets/button';
31
- import { ToggleWidget } from './widgets/toggle';
32
- import { SliderWidget } from './widgets/slider';
33
- import { ImageWidget } from './widgets/image';
34
- export {
35
- NumberWidget, ColorWidget, StringWidget, TextWidget,
36
- ButtonWidget, ToggleWidget, SliderWidget, ImageWidget
37
- };
38
-
39
- import { GlobalWidgetFactory } from './widgets/factory';
40
- export { GlobalWidgetFactory };
1
+ export * from './graph';
2
+ export * from './node';
3
+
4
+ import { ContextMenuConfig, ContextMenuItemConfig, ContextMenuItemState } from './contextMenu';
5
+ export { ContextMenuConfig, ContextMenuItemConfig, ContextMenuItemState };
6
+
7
+ import { Theme } from './theme';
8
+ export { Theme };
9
+
10
+ import { Publisher, PublisherConfig } from './nodes/publisher';
11
+ export { Publisher, PublisherConfig };
12
+
13
+ import { FlowNote, FlowNoteConfig, NoteContentChangeCallback, NoteWidthChangeCallback } from './notes/note';
14
+ export { FlowNote, FlowNoteConfig, NoteContentChangeCallback, NoteWidthChangeCallback };
15
+
16
+ import {
17
+ NoteAddedCallback, NoteDragStartCallback, NoteDragStopCallback,
18
+ NoteRemovedCallback, NoteSubsystemConfig
19
+ } from './notes/subsystem';
20
+ export {
21
+ NoteAddedCallback, NoteDragStartCallback, NoteDragStopCallback,
22
+ NoteRemovedCallback, NoteSubsystemConfig
23
+ };
24
+
25
+ // Ports
26
+ import { Port, PortConfig, PortStyle, PortType, portsCompatible } from './port';
27
+ export { Port, PortConfig, PortStyle, PortType, portsCompatible };
28
+
29
+ // Connections
30
+ import {
31
+ Connection, ConnectionRenderer, ConnectionRendererParams, DefaultConnectionRenderer
32
+ } from './connection';
33
+ export { Connection, ConnectionRenderer, ConnectionRendererParams, DefaultConnectionRenderer };
34
+
35
+ // Node factory
36
+ import { NodeCreatedCallback, NodeFactory, NodeFactoryConfig } from './nodes/factory';
37
+ export { NodeCreatedCallback, NodeFactory, NodeFactoryConfig };
38
+
39
+ // Widgets
40
+ import { Widget } from './widgets/widget';
41
+ import { NumberWidget, NumberWidgetConfig } from './widgets/number';
42
+ import { ColorWidget, ColorWidgetConfig } from './widgets/color';
43
+ import { StringWidget, StringWidgetConfig } from './widgets/string';
44
+ import { TextWidget, TextWidgetConfig } from './widgets/text';
45
+ import { ButtonWidget, ButtonWidgetConfig } from './widgets/button';
46
+ import { ToggleWidget, ToggleWidgetConfig, ToggleStyleConfig } from './widgets/toggle';
47
+ import { SliderWidget, SliderWidgetConfig } from './widgets/slider';
48
+ import { ImageWidget, ImageWidgetConfig } from './widgets/image';
49
+ export {
50
+ Widget,
51
+ NumberWidget, ColorWidget, StringWidget, TextWidget,
52
+ ButtonWidget, ToggleWidget, SliderWidget, ImageWidget
53
+ };
54
+ export type {
55
+ NumberWidgetConfig, ColorWidgetConfig, StringWidgetConfig, TextWidgetConfig,
56
+ ButtonWidgetConfig, ToggleWidgetConfig, ToggleStyleConfig, SliderWidgetConfig,
57
+ ImageWidgetConfig
58
+ };
59
+
60
+ import { GlobalWidgetFactory, WidgetBuilder, WidgetFactory } from './widgets/factory';
61
+ export { GlobalWidgetFactory, WidgetFactory };
62
+ export type { WidgetBuilder };
63
+
64
+ import { Box } from './types/box';
65
+ import { Vector2 } from './types/vector2';
66
+ export type { Box, Vector2 };
@@ -7,6 +7,13 @@ export interface MarkdownEntry {
7
7
  render(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, maxWidth: number): number
8
8
  }
9
9
 
10
+ function onFontsLoaded(callback: () => void): void {
11
+ if (typeof document === "undefined" || document.fonts === undefined) {
12
+ return;
13
+ }
14
+ document.fonts.addEventListener("loadingdone", () => callback());
15
+ }
16
+
10
17
  export class CodeBlockEntry {
11
18
 
12
19
  #text: Text
@@ -17,6 +24,10 @@ export class CodeBlockEntry {
17
24
 
18
25
  #calculatedForWidth: number;
19
26
 
27
+ text(): Text {
28
+ return this.#text;
29
+ }
30
+
20
31
  constructor(text: Text) {
21
32
  this.#text = text;
22
33
  this.#calculatedForWidth = -1
@@ -24,7 +35,7 @@ export class CodeBlockEntry {
24
35
  this.#calculatedEntries = new List<Text>();
25
36
  this.#calculatedPositions = new List<Vector2>();
26
37
 
27
- document.fonts.addEventListener("loadingdone", (event) => {
38
+ onFontsLoaded(() => {
28
39
  this.#calculatedForWidth = -1
29
40
  });
30
41
  }
@@ -113,6 +124,10 @@ export class UnorderedListMarkdownEntry {
113
124
  this.#entries = entries;
114
125
  }
115
126
 
127
+ entries(): ReadonlyArray<BasicMarkdownEntry> {
128
+ return this.#entries;
129
+ }
130
+
116
131
  render(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, maxWidth: number): number {
117
132
  let offset = 0;
118
133
  let shift = 20;
@@ -152,6 +167,10 @@ export class BasicMarkdownEntry {
152
167
 
153
168
  #calculatedForWidth: number;
154
169
 
170
+ lines(): ReadonlyArray<Text> {
171
+ return this.#entries;
172
+ }
173
+
155
174
  constructor(lines: Array<Text>, underline: boolean, background: boolean) {
156
175
  this.#entries = lines;
157
176
  this.#underline = underline;
@@ -161,7 +180,7 @@ export class BasicMarkdownEntry {
161
180
  this.#calculatedEntries = new List<Text>();
162
181
  this.#calculatedPositions = new List<Vector2>();
163
182
 
164
- document.fonts.addEventListener("loadingdone", (event) => {
183
+ onFontsLoaded(() => {
165
184
  this.#calculatedForWidth = -1
166
185
  });
167
186
  }
@@ -37,32 +37,27 @@ export class MarkdownLexicalParser {
37
37
  return this.#body.charAt(this.#index);
38
38
  }
39
39
 
40
- #lastToken = 0;
41
-
42
- #addToken(token: MarkdownTokenType, lexeme: string): void {
43
- this.#tokens.push(new MarkdownToken(token, lexeme, this.#lastToken, this.#index));
44
- this.#lastToken = this.#index;
45
- }
40
+ #tokenStart = 0;
46
41
 
47
- #h2(): void {
48
- let char = this.#next();
49
- if (char === "#") {
50
- this.#addToken(MarkdownTokenType.H3, "###");
42
+ #addToken(token: MarkdownTokenType, lexeme: string, start?: number): void {
43
+ this.#tokens.push(new MarkdownToken(token, lexeme, start ?? this.#tokenStart, this.#index));
44
+ }
51
45
 
52
- // Move it off the character for the next reading
46
+ #header(): void {
47
+ let count = 0;
48
+ while (this.#current() === "#") {
49
+ count++;
53
50
  this.#inc();
54
- } else {
55
- this.#addToken(MarkdownTokenType.H2, "##");
56
51
  }
57
- }
58
52
 
59
- #h1(): void {
60
- let char = this.#next();
61
- if (char === "#") {
62
- this.#h2();
63
- } else {
64
- this.#addToken(MarkdownTokenType.H1, "#");
53
+ let type = MarkdownTokenType.H3;
54
+ if (count === 1) {
55
+ type = MarkdownTokenType.H1;
56
+ } else if (count === 2) {
57
+ type = MarkdownTokenType.H2;
65
58
  }
59
+
60
+ this.#addToken(type, "#".repeat(count));
66
61
  }
67
62
 
68
63
  #whiteSpace(): void {
@@ -110,34 +105,29 @@ export class MarkdownLexicalParser {
110
105
  }
111
106
 
112
107
  if (started != -1) {
113
- this.#addToken(MarkdownTokenType.Text, this.#body.substring(started, this.#index))
108
+ this.#addToken(MarkdownTokenType.Text, this.#body.substring(started, this.#index), started)
114
109
  }
115
110
  }
116
111
 
117
- // S( ) => S
118
- // S(\t) => S
119
- // S(#) => H1
120
- // H1(#) => H2
121
- // H1(!#) => S
122
- // H2(#) => H3
123
- // H2(#) => H3
124
112
  parse(): void {
125
113
  let char = this.#current();
126
114
 
127
115
  while (char !== "") {
116
+ this.#tokenStart = this.#index;
117
+
128
118
  if (char === " " || char === "\t") {
129
119
  this.#whiteSpace();
130
120
  } else if (char === "#") {
131
- this.#h1();
121
+ this.#header();
132
122
  } else if (char === "\n") {
133
- this.#addToken(MarkdownTokenType.NewLine, "\n");
134
123
  this.#inc();
124
+ this.#addToken(MarkdownTokenType.NewLine, "\n");
135
125
  } else if (char === "*") {
136
- this.#addToken(MarkdownTokenType.Star, "*");
137
126
  this.#inc();
127
+ this.#addToken(MarkdownTokenType.Star, "*");
138
128
  } else if (char === "`") {
139
- this.#addToken(MarkdownTokenType.BackTick, "`");
140
129
  this.#inc();
130
+ this.#addToken(MarkdownTokenType.BackTick, "`");
141
131
  } else {
142
132
  this.#text();
143
133
  }
@@ -31,21 +31,18 @@ export class MarkdownSyntaxParser {
31
31
 
32
32
  #next(): MarkdownToken | null {
33
33
  this.#index++
34
- if (this.#index >= this.#tokens.length - 1) {
34
+ if (this.#index >= this.#tokens.length) {
35
35
  return null
36
36
  }
37
37
  return this.#tokens[this.#index];
38
38
  }
39
39
 
40
40
  #peak(): MarkdownToken | null {
41
- if (this.#index + 1 >= this.#tokens.length - 1) {
42
- return null
43
- }
44
- return this.#tokens[this.#index + 1];
41
+ return this.#peakInto(1);
45
42
  }
46
43
 
47
44
  #peakInto(amount: number): MarkdownToken | null {
48
- if (this.#index + amount >= this.#tokens.length - 1) {
45
+ if (this.#index + amount >= this.#tokens.length) {
49
46
  return null
50
47
  }
51
48
  return this.#tokens[this.#index + amount];
@@ -53,82 +50,107 @@ export class MarkdownSyntaxParser {
53
50
 
54
51
  #emphasis(): Array<Text> {
55
52
 
56
- // Cases to consider
57
- // =================
58
- // 1. *a*: <i>a</i>
59
- // 2. **a**: <b>a</b>
60
- // 3. **a*: *<i>a</i> <----- fuck this one
61
- // 4. ***a**: *<b>a</b> <----- fuck this one
62
- // 5. *a**: <i>a</i>*
63
- // 6. **a***: <b>a</b>*
64
- // 7. * a *: * a *
65
- // 8. ** a **: ** a **
66
- // 9. **a **: **a **
67
- // 10. **a f**: <b>a f<b>
68
- // 11. * Test: <ul>Test</ul>
69
-
70
- let token = this.#next();
71
- if (token?.type() === MarkdownTokenType.Space) {
53
+ // Count the stars either side of the content and match as many as
54
+ // both ends can supply; the rest stay on the line as characters.
55
+ //
56
+ // *a* -> <i>a</i> **a* -> *<i>a</i>
57
+ // **a** -> <b>a</b> *a** -> <i>a</i>*
58
+ // **a f** -> <b>a f</b> *a -> *a
59
+ // ** a ** -> ** a ** **a ** -> **a **
60
+
61
+ // A star with a space after it opens nothing, so leave the space
62
+ // where it is rather than consuming it - the caller still has to
63
+ // read it as part of the line.
64
+ if (this.#peak()?.type() === MarkdownTokenType.Space) {
72
65
  return [new Text("*")];
73
66
  }
74
67
 
75
- let bold = false;
76
- // Read off all leading stars
77
- while (token !== null && token.type() === MarkdownTokenType.Star) {
78
- bold = true;
79
- token = this.#next();
80
- }
81
-
82
- let textContent = "";
68
+ const openers = this.#runOfStars();
83
69
 
84
- let boldClosed = false;
85
- let startingClose = false;
86
- let validClose = false;
87
- while (token !== null && token.type() !== MarkdownTokenType.NewLine) {
70
+ let content = "";
71
+ let closers = 0;
72
+ while (true) {
73
+ const token = this.#current();
74
+ if (token === null || token.type() === MarkdownTokenType.NewLine) {
75
+ break;
76
+ }
88
77
  if (token.type() === MarkdownTokenType.Star) {
78
+ closers = this.#runOfStars();
79
+ break;
80
+ }
81
+ content += token.lexeme();
82
+ this.#inc();
83
+ }
89
84
 
90
- // This is the second star we've seen in a row. We're done!
91
- if (startingClose) {
92
- boldClosed = true;
93
- break;
94
- }
85
+ // Leave the reader on the last token taken, since the caller steps
86
+ // forward once more before looking at anything.
87
+ this.#index--;
95
88
 
96
- // If we're not bold, we just need a single star to close this off.
97
- startingClose = true;
98
- if (bold === false) {
99
- break;
100
- }
101
- }
89
+ // A run of stars only emphasises what it hugs. With a space against
90
+ // the inside of either end, or nothing between them at all, the
91
+ // stars are just stars.
92
+ const hugsContent = content !== ""
93
+ && content.trimStart() === content
94
+ && content.trimEnd() === content;
102
95
 
103
- if (startingClose && token.type() !== MarkdownTokenType.Star) {
104
- break;
105
- }
96
+ const matched = hugsContent ? Math.min(openers, closers) : 0;
97
+ if (matched === 0) {
98
+ return [new Text("*".repeat(openers) + content + "*".repeat(closers))];
99
+ }
106
100
 
107
- // Add to string content...
108
- if (token.type() === MarkdownTokenType.Text) {
109
- textContent += token.lexeme();
110
- validClose = true;
111
- }
101
+ const style: TextStyleConfig = {};
102
+ if (matched >= 2) {
103
+ style.weight = FontWeight.Bold;
104
+ } else {
105
+ style.style = FontStyle.Italic;
106
+ }
112
107
 
113
- if (token.type() === MarkdownTokenType.Space) {
114
- textContent += token.lexeme();
115
- validClose = false;
116
- }
108
+ // Stars past the pair that matched were never part of the emphasis,
109
+ // so they stay on the line as the characters they are.
110
+ const runs = new Array<Text>();
111
+ if (openers > matched) {
112
+ runs.push(new Text("*".repeat(openers - matched)));
113
+ }
114
+ runs.push(new Text(content, style));
115
+ if (closers > matched) {
116
+ runs.push(new Text("*".repeat(closers - matched)));
117
+ }
118
+ return runs;
119
+ }
117
120
 
118
- token = this.#next();
121
+ /** Consumes a run of stars, leaving the reader on the token after it. */
122
+ #runOfStars(): number {
123
+ let count = 0;
124
+ while (this.#current()?.type() === MarkdownTokenType.Star) {
125
+ count++;
126
+ this.#inc();
119
127
  }
128
+ return count;
129
+ }
120
130
 
121
- const style: TextStyleConfig = {}
131
+ /**
132
+ * Reads `code` up to the closing backtick on the same line. An opening
133
+ * backtick with no partner is only a backtick.
134
+ */
135
+ #inlineCode(): Array<Text> {
136
+ const opened = this.#index;
122
137
 
123
- if (validClose && startingClose) {
124
- if (boldClosed) {
125
- style.weight = FontWeight.Bold;
126
- } else {
127
- style.style = FontStyle.Italic;
138
+ this.#inc();
139
+ let content = "";
140
+ while (true) {
141
+ const token = this.#current();
142
+ if (token === null || token.type() === MarkdownTokenType.NewLine) {
143
+ this.#index = opened;
144
+ return [new Text("`")];
128
145
  }
146
+ if (token.type() === MarkdownTokenType.BackTick) {
147
+ break;
148
+ }
149
+ content += token.lexeme();
150
+ this.#inc();
129
151
  }
130
152
 
131
- return [new Text(textContent, style)]
153
+ return [new Text(content, { font: "monospace" })];
132
154
  }
133
155
 
134
156
  #text(): Array<Text> {
@@ -153,13 +175,16 @@ export class MarkdownSyntaxParser {
153
175
  break;
154
176
 
155
177
  case MarkdownTokenType.Star:
178
+ case MarkdownTokenType.BackTick:
156
179
  if (textContent !== "") {
157
180
  contents.push(new Text(textContent))
158
181
  textContent = "";
159
182
  }
160
- let starText = this.#emphasis()
161
- for (let i = 0; i < starText.length; i++) {
162
- contents.push(starText[i]);
183
+ const runs = token.type() === MarkdownTokenType.Star
184
+ ? this.#emphasis()
185
+ : this.#inlineCode();
186
+ for (let i = 0; i < runs.length; i++) {
187
+ contents.push(runs[i]);
163
188
  }
164
189
  break;
165
190
  }