gloomberb 0.4.1 → 0.5.0
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/README.md +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/shell.test.tsx +186 -27
- package/src/components/layout/shell.tsx +5 -4
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +62 -18
- package/src/plugins/builtin/chat-controller.ts +23 -12
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +415 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
package/src/theme/themes.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Theme {
|
|
|
17
17
|
positive: string;
|
|
18
18
|
negative: string;
|
|
19
19
|
neutral: string;
|
|
20
|
+
warning: string;
|
|
20
21
|
|
|
21
22
|
header: string;
|
|
22
23
|
headerText: string;
|
|
@@ -79,6 +80,12 @@ function normalizeTheme(theme: Theme): Theme {
|
|
|
79
80
|
highestMinimumContrast(bodySurfaces, [textDim, text, "#c0c7d1"]),
|
|
80
81
|
SUBTLE_TEXT_MIN,
|
|
81
82
|
);
|
|
83
|
+
const warning = blendForContrastOnSurfaces(
|
|
84
|
+
theme.warning,
|
|
85
|
+
bodySurfaces,
|
|
86
|
+
blendHex(theme.warning, theme.textBright, 0.5),
|
|
87
|
+
SUBTLE_TEXT_MIN,
|
|
88
|
+
);
|
|
82
89
|
const selected = blendForContrastOnSurfaces(
|
|
83
90
|
theme.selected,
|
|
84
91
|
bodySurfaces,
|
|
@@ -106,6 +113,7 @@ function normalizeTheme(theme: Theme): Theme {
|
|
|
106
113
|
positive,
|
|
107
114
|
negative,
|
|
108
115
|
neutral,
|
|
116
|
+
warning,
|
|
109
117
|
headerText,
|
|
110
118
|
selected,
|
|
111
119
|
selectedText,
|
|
@@ -127,6 +135,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
127
135
|
positive: "#00cc66",
|
|
128
136
|
negative: "#ff3333",
|
|
129
137
|
neutral: "#888888",
|
|
138
|
+
warning: "#ffaa00",
|
|
130
139
|
header: "#0044aa",
|
|
131
140
|
headerText: "#ffffff",
|
|
132
141
|
selected: "#1a3a5c",
|
|
@@ -149,6 +158,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
149
158
|
positive: "#00ff66",
|
|
150
159
|
negative: "#ff4444",
|
|
151
160
|
neutral: "#666666",
|
|
161
|
+
warning: "#cccc00",
|
|
152
162
|
header: "#003300",
|
|
153
163
|
headerText: "#00ff00",
|
|
154
164
|
selected: "#003300",
|
|
@@ -171,6 +181,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
171
181
|
positive: "#00ff88",
|
|
172
182
|
negative: "#ff4444",
|
|
173
183
|
neutral: "#666666",
|
|
184
|
+
warning: "#ddcc00",
|
|
174
185
|
header: "#002222",
|
|
175
186
|
headerText: "#00ffff",
|
|
176
187
|
selected: "#002a2a",
|
|
@@ -193,6 +204,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
193
204
|
positive: "#44cc44",
|
|
194
205
|
negative: "#ff6666",
|
|
195
206
|
neutral: "#666666",
|
|
207
|
+
warning: "#ddaa00",
|
|
196
208
|
header: "#220000",
|
|
197
209
|
headerText: "#ff3333",
|
|
198
210
|
selected: "#2a0000",
|
|
@@ -215,6 +227,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
215
227
|
positive: "#00cc66",
|
|
216
228
|
negative: "#ff4444",
|
|
217
229
|
neutral: "#666666",
|
|
230
|
+
warning: "#ddaa00",
|
|
218
231
|
header: "#000033",
|
|
219
232
|
headerText: "#4488ff",
|
|
220
233
|
selected: "#001144",
|
|
@@ -237,6 +250,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
237
250
|
positive: "#44dd66",
|
|
238
251
|
negative: "#ff4444",
|
|
239
252
|
neutral: "#666666",
|
|
253
|
+
warning: "#ddaa00",
|
|
240
254
|
header: "#1a0028",
|
|
241
255
|
headerText: "#bb55ff",
|
|
242
256
|
selected: "#220033",
|
|
@@ -259,6 +273,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
259
273
|
positive: "#44dd66",
|
|
260
274
|
negative: "#ff5555",
|
|
261
275
|
neutral: "#666666",
|
|
276
|
+
warning: "#ddaa00",
|
|
262
277
|
header: "#22000f",
|
|
263
278
|
headerText: "#ff44aa",
|
|
264
279
|
selected: "#2a0018",
|
|
@@ -281,6 +296,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
281
296
|
positive: "#00cc66",
|
|
282
297
|
negative: "#ff3333",
|
|
283
298
|
neutral: "#888888",
|
|
299
|
+
warning: "#ddaa00",
|
|
284
300
|
header: "#1a1a1a",
|
|
285
301
|
headerText: "#ffffff",
|
|
286
302
|
selected: "#222222",
|
|
@@ -303,6 +319,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
303
319
|
positive: "#9ece6a",
|
|
304
320
|
negative: "#f7768e",
|
|
305
321
|
neutral: "#565f89",
|
|
322
|
+
warning: "#e0af68",
|
|
306
323
|
header: "#24283b",
|
|
307
324
|
headerText: "#7aa2f7",
|
|
308
325
|
selected: "#283457",
|
|
@@ -325,6 +342,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
325
342
|
positive: "#859900",
|
|
326
343
|
negative: "#dc322f",
|
|
327
344
|
neutral: "#657b83",
|
|
345
|
+
warning: "#b58900",
|
|
328
346
|
header: "#073642",
|
|
329
347
|
headerText: "#268bd2",
|
|
330
348
|
selected: "#0a4a5c",
|
|
@@ -347,6 +365,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
347
365
|
positive: "#50fa7b",
|
|
348
366
|
negative: "#ff5555",
|
|
349
367
|
neutral: "#6272a4",
|
|
368
|
+
warning: "#f1fa8c",
|
|
350
369
|
header: "#44475a",
|
|
351
370
|
headerText: "#bd93f9",
|
|
352
371
|
selected: "#44475a",
|
|
@@ -369,6 +388,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
369
388
|
positive: "#a3be8c",
|
|
370
389
|
negative: "#bf616a",
|
|
371
390
|
neutral: "#4c566a",
|
|
391
|
+
warning: "#ebcb8b",
|
|
372
392
|
header: "#3b4252",
|
|
373
393
|
headerText: "#88c0d0",
|
|
374
394
|
selected: "#4c566a",
|
|
@@ -391,6 +411,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
391
411
|
positive: "#a6e22e",
|
|
392
412
|
negative: "#f92672",
|
|
393
413
|
neutral: "#75715e",
|
|
414
|
+
warning: "#e6db74",
|
|
394
415
|
header: "#49483e",
|
|
395
416
|
headerText: "#e6db74",
|
|
396
417
|
selected: "#49483e",
|
|
@@ -413,6 +434,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
413
434
|
positive: "#a6e3a1",
|
|
414
435
|
negative: "#f38ba8",
|
|
415
436
|
neutral: "#6c7086",
|
|
437
|
+
warning: "#f9e2af",
|
|
416
438
|
header: "#313244",
|
|
417
439
|
headerText: "#cba6f7",
|
|
418
440
|
selected: "#3e3f56",
|
|
@@ -435,6 +457,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
435
457
|
positive: "#b8bb26",
|
|
436
458
|
negative: "#fb4934",
|
|
437
459
|
neutral: "#928374",
|
|
460
|
+
warning: "#fabd2f",
|
|
438
461
|
header: "#3c3836",
|
|
439
462
|
headerText: "#fabd2f",
|
|
440
463
|
selected: "#3c3836",
|
|
@@ -457,6 +480,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
457
480
|
positive: "#31748f",
|
|
458
481
|
negative: "#eb6f92",
|
|
459
482
|
neutral: "#6e6a86",
|
|
483
|
+
warning: "#f6c177",
|
|
460
484
|
header: "#26233a",
|
|
461
485
|
headerText: "#c4a7e7",
|
|
462
486
|
selected: "#332f4a",
|
|
@@ -479,6 +503,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
479
503
|
positive: "#00cc66",
|
|
480
504
|
negative: "#ff3333",
|
|
481
505
|
neutral: "#6688bb",
|
|
506
|
+
warning: "#ff9900",
|
|
482
507
|
header: "#001144",
|
|
483
508
|
headerText: "#ff6600",
|
|
484
509
|
selected: "#002255",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createPaneInstance,
|
|
4
|
+
findPrimaryPaneInstance,
|
|
5
|
+
resolveFollowBindingInstance,
|
|
6
|
+
resolvePaneInstance,
|
|
7
|
+
type LayoutConfig,
|
|
8
|
+
type PaneInstanceConfig,
|
|
9
|
+
} from "./config";
|
|
10
|
+
|
|
11
|
+
function createLayout(instances: PaneInstanceConfig[]): LayoutConfig {
|
|
12
|
+
return {
|
|
13
|
+
dockRoot: null,
|
|
14
|
+
instances,
|
|
15
|
+
floating: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("findPrimaryPaneInstance", () => {
|
|
20
|
+
test("prefers the main non-fixed ticker pane", () => {
|
|
21
|
+
const layout = createLayout([
|
|
22
|
+
createPaneInstance("portfolio-list", {
|
|
23
|
+
instanceId: "portfolio-list:main",
|
|
24
|
+
binding: { kind: "none" },
|
|
25
|
+
}),
|
|
26
|
+
createPaneInstance("ticker-detail", {
|
|
27
|
+
instanceId: "ticker-detail:pinned",
|
|
28
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
29
|
+
}),
|
|
30
|
+
createPaneInstance("ticker-detail", {
|
|
31
|
+
instanceId: "ticker-detail:main",
|
|
32
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
expect(findPrimaryPaneInstance(layout, "ticker-detail")?.instanceId).toBe("ticker-detail:main");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("does not treat fixed ticker panes as the primary pane", () => {
|
|
40
|
+
const layout = createLayout([
|
|
41
|
+
createPaneInstance("ticker-detail", {
|
|
42
|
+
instanceId: "ticker-detail:aapl",
|
|
43
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
44
|
+
}),
|
|
45
|
+
createPaneInstance("ticker-detail", {
|
|
46
|
+
instanceId: "ticker-detail:msft",
|
|
47
|
+
binding: { kind: "fixed", symbol: "MSFT" },
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
expect(findPrimaryPaneInstance(layout, "ticker-detail")).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("resolvePaneInstance", () => {
|
|
56
|
+
test("accepts either an instance id or a pane id", () => {
|
|
57
|
+
const layout = createLayout([
|
|
58
|
+
createPaneInstance("portfolio-list", {
|
|
59
|
+
instanceId: "portfolio-list:main",
|
|
60
|
+
binding: { kind: "none" },
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
expect(resolvePaneInstance(layout, "portfolio-list:main")?.instanceId).toBe("portfolio-list:main");
|
|
65
|
+
expect(resolvePaneInstance(layout, "portfolio-list")?.instanceId).toBe("portfolio-list:main");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("resolveFollowBindingInstance", () => {
|
|
70
|
+
test("walks follow bindings until it finds a matching pane", () => {
|
|
71
|
+
const layout = createLayout([
|
|
72
|
+
createPaneInstance("portfolio-list", {
|
|
73
|
+
instanceId: "portfolio-list:main",
|
|
74
|
+
binding: { kind: "none" },
|
|
75
|
+
}),
|
|
76
|
+
createPaneInstance("ticker-detail", {
|
|
77
|
+
instanceId: "ticker-detail:main",
|
|
78
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
79
|
+
}),
|
|
80
|
+
createPaneInstance("quote-monitor", {
|
|
81
|
+
instanceId: "quote-monitor:main",
|
|
82
|
+
binding: { kind: "follow", sourceInstanceId: "ticker-detail:main" },
|
|
83
|
+
}),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
resolveFollowBindingInstance(layout, "quote-monitor:main", (instance) => instance.paneId === "portfolio-list")?.instanceId,
|
|
88
|
+
).toBe("portfolio-list:main");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("stops on follow cycles", () => {
|
|
92
|
+
const layout = createLayout([
|
|
93
|
+
createPaneInstance("ticker-detail", {
|
|
94
|
+
instanceId: "ticker-detail:first",
|
|
95
|
+
binding: { kind: "follow", sourceInstanceId: "quote-monitor:first" },
|
|
96
|
+
}),
|
|
97
|
+
createPaneInstance("quote-monitor", {
|
|
98
|
+
instanceId: "quote-monitor:first",
|
|
99
|
+
binding: { kind: "follow", sourceInstanceId: "ticker-detail:first" },
|
|
100
|
+
}),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
resolveFollowBindingInstance(layout, "ticker-detail:first", (instance) => instance.paneId === "portfolio-list"),
|
|
105
|
+
).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/types/config.ts
CHANGED
|
@@ -179,7 +179,7 @@ export const DEFAULT_LAYOUT: LayoutConfig = {
|
|
|
179
179
|
hideTabs: false,
|
|
180
180
|
lockedTabId: "overview",
|
|
181
181
|
chartRangePreset: "5Y",
|
|
182
|
-
chartResolution: "
|
|
182
|
+
chartResolution: "auto",
|
|
183
183
|
},
|
|
184
184
|
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
185
185
|
},
|
|
@@ -317,6 +317,35 @@ export function isFixedTickerPane(instance: PaneInstanceConfig): boolean {
|
|
|
317
317
|
return isTickerPaneInstance(instance) && instance.binding?.kind === "fixed";
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
export function findPrimaryPaneInstance(layout: LayoutConfig, paneId: string): PaneInstanceConfig | undefined {
|
|
321
|
+
const instances = layout.instances.filter((instance) => instance.paneId === paneId);
|
|
322
|
+
if (instances.length === 0) return undefined;
|
|
323
|
+
if (!isTickerPaneId(paneId)) return instances[0];
|
|
324
|
+
return instances.find((instance) => instance.instanceId === `${paneId}:main` && instance.binding?.kind !== "fixed")
|
|
325
|
+
?? instances.find((instance) => instance.binding?.kind !== "fixed");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function resolvePaneInstance(layout: LayoutConfig, paneIdOrInstanceId: string): PaneInstanceConfig | undefined {
|
|
329
|
+
return findPaneInstance(layout, paneIdOrInstanceId) ?? findPrimaryPaneInstance(layout, paneIdOrInstanceId);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function resolveFollowBindingInstance(
|
|
333
|
+
layout: LayoutConfig,
|
|
334
|
+
paneIdOrInstanceId: string | null | undefined,
|
|
335
|
+
matcher: (instance: PaneInstanceConfig) => boolean,
|
|
336
|
+
seen = new Set<string>(),
|
|
337
|
+
): PaneInstanceConfig | undefined {
|
|
338
|
+
if (!paneIdOrInstanceId) return undefined;
|
|
339
|
+
const instance = resolvePaneInstance(layout, paneIdOrInstanceId);
|
|
340
|
+
if (!instance || seen.has(instance.instanceId)) return undefined;
|
|
341
|
+
seen.add(instance.instanceId);
|
|
342
|
+
if (matcher(instance)) return instance;
|
|
343
|
+
if (instance.binding?.kind === "follow") {
|
|
344
|
+
return resolveFollowBindingInstance(layout, instance.binding.sourceInstanceId, matcher, seen);
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
320
349
|
export function createPaneInstance(
|
|
321
350
|
paneId: string,
|
|
322
351
|
options: Partial<PaneInstanceConfig> = {},
|
|
@@ -26,6 +26,18 @@ export interface SecFilingItem {
|
|
|
26
26
|
primaryDocumentUrl?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface EarningsEvent {
|
|
30
|
+
symbol: string;
|
|
31
|
+
name: string;
|
|
32
|
+
earningsDate: Date;
|
|
33
|
+
epsEstimate: number | null;
|
|
34
|
+
epsActual: number | null;
|
|
35
|
+
revenueEstimate: number | null;
|
|
36
|
+
revenueActual: number | null;
|
|
37
|
+
surprise: number | null;
|
|
38
|
+
timing: "BMO" | "AMC" | "TNS" | "";
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
export interface MarketDataRequestContext {
|
|
30
42
|
brokerId?: string;
|
|
31
43
|
brokerInstanceId?: string;
|
|
@@ -68,6 +80,7 @@ export interface DataProvider {
|
|
|
68
80
|
search(query: string, context?: SearchRequestContext): Promise<InstrumentSearchResult[]>;
|
|
69
81
|
getNews(ticker: string, count?: number, exchange?: string, context?: MarketDataRequestContext): Promise<NewsItem[]>;
|
|
70
82
|
getSecFilings?(ticker: string, count?: number, exchange?: string, context?: MarketDataRequestContext): Promise<SecFilingItem[]>;
|
|
83
|
+
getEarningsCalendar?(symbols: string[], context?: MarketDataRequestContext): Promise<EarningsEvent[]>;
|
|
71
84
|
getSecFilingContent?(filing: SecFilingItem): Promise<string | null>;
|
|
72
85
|
/** Fetch article summary/description by URL (lazy-loaded on selection) */
|
|
73
86
|
getArticleSummary(url: string): Promise<string | null>;
|
package/src/types/plugin.ts
CHANGED
|
@@ -169,6 +169,19 @@ export interface WizardStep {
|
|
|
169
169
|
body?: string[];
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
export interface CommandShortcutArgContext {
|
|
173
|
+
activeTicker: string | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface CommandShortcutArgDef {
|
|
177
|
+
placeholder?: string;
|
|
178
|
+
kind?: "text" | "ticker" | "ticker-list";
|
|
179
|
+
parse?: (
|
|
180
|
+
arg: string,
|
|
181
|
+
context: CommandShortcutArgContext,
|
|
182
|
+
) => Record<string, string>;
|
|
183
|
+
}
|
|
184
|
+
|
|
172
185
|
export interface CliHelpColumn {
|
|
173
186
|
header: string;
|
|
174
187
|
align?: "left" | "right" | "center";
|
|
@@ -243,6 +256,7 @@ export interface CommandDef {
|
|
|
243
256
|
label: string;
|
|
244
257
|
keywords: string[];
|
|
245
258
|
shortcut?: string;
|
|
259
|
+
shortcutArg?: CommandShortcutArgDef;
|
|
246
260
|
execute: (values?: Record<string, string>) => void | Promise<void>;
|
|
247
261
|
category: "navigation" | "data" | "portfolio" | "config";
|
|
248
262
|
description?: string;
|
|
@@ -355,6 +369,25 @@ export interface PluginPaneSettingsState {
|
|
|
355
369
|
delete(paneId: string, key: string): Promise<void>;
|
|
356
370
|
}
|
|
357
371
|
|
|
372
|
+
export type AppNotificationType = "info" | "success" | "error";
|
|
373
|
+
export type AppDesktopNotificationMode = "never" | "when-inactive" | "always";
|
|
374
|
+
|
|
375
|
+
export interface AppNotificationRequest {
|
|
376
|
+
title?: string;
|
|
377
|
+
body: string;
|
|
378
|
+
subtitle?: string;
|
|
379
|
+
duration?: number;
|
|
380
|
+
type?: AppNotificationType;
|
|
381
|
+
toast?: boolean;
|
|
382
|
+
desktop?: AppDesktopNotificationMode;
|
|
383
|
+
persistent?: boolean;
|
|
384
|
+
sound?: string;
|
|
385
|
+
action?: {
|
|
386
|
+
label: string;
|
|
387
|
+
onClick: () => void;
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
358
391
|
export interface GloomPluginContext {
|
|
359
392
|
registerPane(pane: PaneDef): void;
|
|
360
393
|
registerPaneTemplate(template: PaneTemplateDef): void;
|
|
@@ -392,6 +425,7 @@ export interface GloomPluginContext {
|
|
|
392
425
|
hidePane(paneId: string): void;
|
|
393
426
|
focusPane(paneId: string): void;
|
|
394
427
|
pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
|
|
428
|
+
navigateTicker(symbol: string): void;
|
|
395
429
|
openPaneSettings(paneId?: string): void;
|
|
396
430
|
|
|
397
431
|
on<K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void): () => void;
|
|
@@ -399,7 +433,7 @@ export interface GloomPluginContext {
|
|
|
399
433
|
|
|
400
434
|
showWidget(widgetId: string): void;
|
|
401
435
|
hideWidget(widgetId: string): void;
|
|
402
|
-
|
|
436
|
+
notify(notification: AppNotificationRequest): void;
|
|
403
437
|
}
|
|
404
438
|
|
|
405
439
|
export interface GloomPlugin {
|
package/src/utils/format.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { formatTimeAgo } from "./format";
|
|
2
|
+
import { displayWidth, formatTimeAgo, padTo } from "./format";
|
|
3
3
|
|
|
4
4
|
describe("formatTimeAgo", () => {
|
|
5
5
|
test("handles UTC ISO timestamps with explicit offsets", () => {
|
|
@@ -12,3 +12,13 @@ describe("formatTimeAgo", () => {
|
|
|
12
12
|
expect(formatTimeAgo(fiveMinutesAgo)).toBe("5m ago");
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
describe("padTo", () => {
|
|
17
|
+
test("pads and truncates by display width instead of UTF-16 length", () => {
|
|
18
|
+
expect(displayWidth("🇺🇸")).toBe(2);
|
|
19
|
+
expect(padTo("🇺🇸", 2)).toBe("🇺🇸");
|
|
20
|
+
expect(padTo("🇺🇸", 3)).toBe("🇺🇸 ");
|
|
21
|
+
expect(padTo("🇺🇸", 1)).toBe(" ");
|
|
22
|
+
expect(padTo("🇺🇸 CPI", 6)).toBe("🇺🇸 CPI");
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/utils/format.ts
CHANGED
|
@@ -116,17 +116,95 @@ export function formatWithDivisor(value: number | undefined, divisor: number): s
|
|
|
116
116
|
return scaled.toFixed(decimals);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
const COMBINING_MARK_RE = /\p{Mark}/u;
|
|
120
|
+
const EMOJI_PRESENTATION_RE = /\p{Emoji_Presentation}/u;
|
|
121
|
+
const EXTENDED_PICTOGRAPHIC_RE = /\p{Extended_Pictographic}/u;
|
|
122
|
+
const REGIONAL_INDICATOR_RE = /\p{Regional_Indicator}/u;
|
|
123
|
+
|
|
124
|
+
function segmentGraphemes(value: string): string[] {
|
|
125
|
+
const Segmenter = (Intl as any).Segmenter;
|
|
126
|
+
if (typeof Segmenter === "function") {
|
|
127
|
+
return Array.from(
|
|
128
|
+
new Segmenter(undefined, { granularity: "grapheme" }).segment(value),
|
|
129
|
+
(entry: any) => entry.segment as string,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return Array.from(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isFullwidthCodePoint(codePoint: number): boolean {
|
|
136
|
+
return codePoint >= 0x1100 && (
|
|
137
|
+
codePoint <= 0x115f ||
|
|
138
|
+
codePoint === 0x2329 ||
|
|
139
|
+
codePoint === 0x232a ||
|
|
140
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
141
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
142
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
143
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
144
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
145
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
146
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
147
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
|
|
148
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function graphemeWidth(segment: string): number {
|
|
153
|
+
if (
|
|
154
|
+
REGIONAL_INDICATOR_RE.test(segment) ||
|
|
155
|
+
EMOJI_PRESENTATION_RE.test(segment) ||
|
|
156
|
+
EXTENDED_PICTOGRAPHIC_RE.test(segment)
|
|
157
|
+
) {
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let width = 0;
|
|
162
|
+
for (const char of Array.from(segment)) {
|
|
163
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
164
|
+
if (
|
|
165
|
+
codePoint === 0 ||
|
|
166
|
+
codePoint < 32 ||
|
|
167
|
+
(codePoint >= 0x7f && codePoint < 0xa0) ||
|
|
168
|
+
codePoint === 0x200d ||
|
|
169
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
|
|
170
|
+
COMBINING_MARK_RE.test(char)
|
|
171
|
+
) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
width += isFullwidthCodePoint(codePoint) ? 2 : 1;
|
|
175
|
+
}
|
|
176
|
+
return width;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function displayWidth(value: string): number {
|
|
180
|
+
return segmentGraphemes(value).reduce((total, segment) => total + graphemeWidth(segment), 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function truncateToWidth(value: string, width: number): string {
|
|
184
|
+
if (width <= 0) return "";
|
|
185
|
+
let output = "";
|
|
186
|
+
let used = 0;
|
|
187
|
+
for (const segment of segmentGraphemes(value)) {
|
|
188
|
+
const nextWidth = graphemeWidth(segment);
|
|
189
|
+
if (used + nextWidth > width) break;
|
|
190
|
+
output += segment;
|
|
191
|
+
used += nextWidth;
|
|
192
|
+
}
|
|
193
|
+
return output;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Pad/truncate a string to a fixed display width */
|
|
120
197
|
export function padTo(str: string, width: number, align: "left" | "right" | "center" = "left"): string {
|
|
121
|
-
|
|
122
|
-
|
|
198
|
+
const clipped = displayWidth(str) > width ? truncateToWidth(str, width) : str;
|
|
199
|
+
const clippedWidth = displayWidth(clipped);
|
|
200
|
+
const padding = Math.max(0, width - clippedWidth);
|
|
201
|
+
if (align === "right") return " ".repeat(padding) + clipped;
|
|
123
202
|
if (align === "center") {
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
return " ".repeat(leftPadding) + str + " ".repeat(rightPadding);
|
|
203
|
+
const leftPadding = Math.floor(padding / 2);
|
|
204
|
+
const rightPadding = padding - leftPadding;
|
|
205
|
+
return " ".repeat(leftPadding) + clipped + " ".repeat(rightPadding);
|
|
128
206
|
}
|
|
129
|
-
return
|
|
207
|
+
return clipped + " ".repeat(padding);
|
|
130
208
|
}
|
|
131
209
|
|
|
132
210
|
/** Convert a value from one currency to base currency using cached exchange rates */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { tokenizeInlineContent } from "./inline-content-tokenizer";
|
|
3
|
+
|
|
4
|
+
describe("tokenizeInlineContent", () => {
|
|
5
|
+
test("returns mixed text, link, and ticker tokens in one pass", () => {
|
|
6
|
+
expect(tokenizeInlineContent("Read https://example.com then watch $NVDA")).toEqual([
|
|
7
|
+
{ kind: "text", value: "Read " },
|
|
8
|
+
{ kind: "link", value: "https://example.com", url: "https://example.com" },
|
|
9
|
+
{ kind: "text", value: " then watch " },
|
|
10
|
+
{ kind: "ticker", value: "$NVDA", symbol: "NVDA" },
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("does not split ticker-like text out of links", () => {
|
|
15
|
+
expect(tokenizeInlineContent("Read https://example.com/$TSLA before $NVDA")).toEqual([
|
|
16
|
+
{ kind: "text", value: "Read " },
|
|
17
|
+
{ kind: "link", value: "https://example.com/$TSLA", url: "https://example.com/$TSLA" },
|
|
18
|
+
{ kind: "text", value: " before " },
|
|
19
|
+
{ kind: "ticker", value: "$NVDA", symbol: "NVDA" },
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { tokenizeInlineLinks } from "./link-tokenizer";
|
|
2
|
+
|
|
3
|
+
export interface InlineContentTextToken {
|
|
4
|
+
kind: "text";
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface InlineContentLinkToken {
|
|
9
|
+
kind: "link";
|
|
10
|
+
value: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface InlineContentTickerToken {
|
|
15
|
+
kind: "ticker";
|
|
16
|
+
value: string;
|
|
17
|
+
symbol: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type InlineContentToken =
|
|
21
|
+
| InlineContentTextToken
|
|
22
|
+
| InlineContentLinkToken
|
|
23
|
+
| InlineContentTickerToken;
|
|
24
|
+
|
|
25
|
+
const TICKER_TOKEN_RE = /\$[A-Z][A-Z0-9.-]{0,9}/g;
|
|
26
|
+
const SYMBOL_CHAR_RE = /[A-Za-z0-9.-]/;
|
|
27
|
+
|
|
28
|
+
function trimTrailingTickerPunctuation(value: string): string {
|
|
29
|
+
let trimmed = value;
|
|
30
|
+
while (trimmed.length > 2 && /[.-]$/.test(trimmed)) {
|
|
31
|
+
trimmed = trimmed.slice(0, -1);
|
|
32
|
+
}
|
|
33
|
+
return trimmed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isValidTickerBoundary(text: string, start: number, end: number): boolean {
|
|
37
|
+
const prev = start > 0 ? text[start - 1] : "";
|
|
38
|
+
const next = end < text.length ? text[end] : "";
|
|
39
|
+
return !SYMBOL_CHAR_RE.test(prev) && !SYMBOL_CHAR_RE.test(next);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function tokenizeInlineContent(text: string): InlineContentToken[] {
|
|
43
|
+
const tokens: InlineContentToken[] = [];
|
|
44
|
+
|
|
45
|
+
for (const segment of tokenizeInlineLinks(text)) {
|
|
46
|
+
if (segment.kind === "link") {
|
|
47
|
+
tokens.push(segment);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let cursor = 0;
|
|
52
|
+
let match: RegExpExecArray | null;
|
|
53
|
+
|
|
54
|
+
TICKER_TOKEN_RE.lastIndex = 0;
|
|
55
|
+
while ((match = TICKER_TOKEN_RE.exec(segment.value)) !== null) {
|
|
56
|
+
const rawValue = match[0];
|
|
57
|
+
const start = match.index;
|
|
58
|
+
const rawEnd = start + rawValue.length;
|
|
59
|
+
const value = trimTrailingTickerPunctuation(rawValue);
|
|
60
|
+
const end = start + value.length;
|
|
61
|
+
if (!isValidTickerBoundary(segment.value, start, rawEnd)) continue;
|
|
62
|
+
|
|
63
|
+
if (start > cursor) {
|
|
64
|
+
tokens.push({ kind: "text", value: segment.value.slice(cursor, start) });
|
|
65
|
+
}
|
|
66
|
+
tokens.push({ kind: "ticker", value, symbol: value.slice(1) });
|
|
67
|
+
cursor = end;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cursor < segment.value.length) {
|
|
71
|
+
tokens.push({ kind: "text", value: segment.value.slice(cursor) });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return tokens;
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { tokenizeInlineLinks } from "./link-tokenizer";
|
|
3
|
+
|
|
4
|
+
describe("tokenizeInlineLinks", () => {
|
|
5
|
+
test("extracts http links and preserves surrounding text", () => {
|
|
6
|
+
expect(tokenizeInlineLinks("Read https://example.com/story now")).toEqual([
|
|
7
|
+
{ kind: "text", value: "Read " },
|
|
8
|
+
{ kind: "link", value: "https://example.com/story", url: "https://example.com/story" },
|
|
9
|
+
{ kind: "text", value: " now" },
|
|
10
|
+
]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("drops trailing punctuation from the link token", () => {
|
|
14
|
+
expect(tokenizeInlineLinks("See https://example.com/story.)")).toEqual([
|
|
15
|
+
{ kind: "text", value: "See " },
|
|
16
|
+
{ kind: "link", value: "https://example.com/story", url: "https://example.com/story" },
|
|
17
|
+
{ kind: "text", value: ".)" },
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("normalizes www links for opening", () => {
|
|
22
|
+
expect(tokenizeInlineLinks("Visit www.example.com/test")).toEqual([
|
|
23
|
+
{ kind: "text", value: "Visit " },
|
|
24
|
+
{ kind: "link", value: "www.example.com/test", url: "https://www.example.com/test" },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
});
|