gloomberb 0.17.2 → 0.17.3
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 +1 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from "../../../ui";
|
|
2
2
|
import type { ChatMessage } from "../../../api-client";
|
|
3
3
|
import { splitLongTextSegmentByDisplayWidth, truncateWithEllipsis } from "../../../utils/text-wrap";
|
|
4
|
-
import {
|
|
4
|
+
import { parseMarkdownLines } from "../../../utils/markdown";
|
|
5
5
|
import { displayWidth } from "../../../utils/format";
|
|
6
6
|
import {
|
|
7
7
|
getTickerBadgeText,
|
|
@@ -12,6 +12,7 @@ import type { InlineTickerCatalogEntry } from "../../../state/hooks/inline-ticke
|
|
|
12
12
|
import { STICKY_BOTTOM_THRESHOLD_PX } from "../../../ui/sticky-scroll";
|
|
13
13
|
import { terminalReactionRows } from "./reactions";
|
|
14
14
|
import { chatMessagePaneLinks } from "./pane-links/model";
|
|
15
|
+
import { chatInlineTokens, type ChatInlineToken, type ChatTextStyle } from "./markdown";
|
|
15
16
|
|
|
16
17
|
const MESSAGE_GROUP_THRESHOLD_MS = 5 * 60 * 1000;
|
|
17
18
|
const MESSAGE_SELECTION_BOTTOM_INSET = 1;
|
|
@@ -38,9 +39,8 @@ export function formatInlinePreview(text: string, width: number) {
|
|
|
38
39
|
return truncateWithEllipsis(normalizeInlinePreview(text), width);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export type ChatBodyLine = ChatBodyInlineToken[];
|
|
42
|
+
/** One row of a message in the terminal: its tokens, already wrapped to the body's width. */
|
|
43
|
+
export type ChatBodyLine = ChatInlineToken[];
|
|
44
44
|
|
|
45
45
|
function wrapTextLines(text: string, width: number) {
|
|
46
46
|
const safeWidth = Math.max(width, 1);
|
|
@@ -97,7 +97,7 @@ export function chatBadgeTextWidth(symbol: string, entry: InlineTickerCatalogEnt
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function inlineTokenWidth(
|
|
100
|
-
token:
|
|
100
|
+
token: ChatInlineToken,
|
|
101
101
|
catalog: Record<string, InlineTickerCatalogEntry>,
|
|
102
102
|
): number {
|
|
103
103
|
if (token.kind !== "ticker") return displayWidth(token.value);
|
|
@@ -110,11 +110,8 @@ function inlineTokenWidth(
|
|
|
110
110
|
return chatBadgeTextWidth(token.symbol, entry) + TICKER_BADGE_CHROME_WIDTH;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function tokenWithValue(token:
|
|
114
|
-
|
|
115
|
-
if (token.kind === "username") return { ...token, value };
|
|
116
|
-
if (token.kind === "ticker") return { ...token, value };
|
|
117
|
-
return { kind: "text", value };
|
|
113
|
+
function tokenWithValue(token: ChatInlineToken, value: string): ChatInlineToken {
|
|
114
|
+
return { ...token, value };
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
function firstDisplayWidthChunk(value: string, width: number): string {
|
|
@@ -136,8 +133,27 @@ function trimLineEnd(line: ChatBodyLine): ChatBodyLine {
|
|
|
136
133
|
return next;
|
|
137
134
|
}
|
|
138
135
|
|
|
139
|
-
function
|
|
140
|
-
|
|
136
|
+
function sameStyle(a: ChatTextStyle | undefined, b: ChatTextStyle | undefined): boolean {
|
|
137
|
+
return !!a?.bold === !!b?.bold && !!a?.italic === !!b?.italic && !!a?.strike === !!b?.strike
|
|
138
|
+
&& !!a?.code === !!b?.code && !!a?.muted === !!b?.muted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Neighbouring words of one style draw as one run. */
|
|
142
|
+
function joinTextTokens(line: ChatBodyLine): ChatBodyLine {
|
|
143
|
+
const joined: ChatBodyLine = [];
|
|
144
|
+
for (const token of line) {
|
|
145
|
+
const previous = joined[joined.length - 1];
|
|
146
|
+
if (token.kind === "text" && previous?.kind === "text" && sameStyle(previous.style, token.style)) {
|
|
147
|
+
joined[joined.length - 1] = { ...previous, value: previous.value + token.value };
|
|
148
|
+
} else {
|
|
149
|
+
joined.push(token);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return joined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function wrapInlineTokens(
|
|
156
|
+
tokens: readonly ChatInlineToken[],
|
|
141
157
|
width: number,
|
|
142
158
|
catalog: Record<string, InlineTickerCatalogEntry>,
|
|
143
159
|
): ChatBodyLine[] {
|
|
@@ -147,12 +163,12 @@ function wrapInlineContentLine(
|
|
|
147
163
|
let currentWidth = 0;
|
|
148
164
|
|
|
149
165
|
const finishLine = () => {
|
|
150
|
-
lines.push(trimLineEnd(current));
|
|
166
|
+
lines.push(joinTextTokens(trimLineEnd(current)));
|
|
151
167
|
current = [];
|
|
152
168
|
currentWidth = 0;
|
|
153
169
|
};
|
|
154
170
|
|
|
155
|
-
const appendSegment = (token:
|
|
171
|
+
const appendSegment = (token: ChatInlineToken, rawValue: string, trimStartOnWrap: boolean) => {
|
|
156
172
|
let value = currentWidth === 0 && trimStartOnWrap ? rawValue.trimStart() : rawValue;
|
|
157
173
|
if (!value) return;
|
|
158
174
|
|
|
@@ -202,53 +218,99 @@ function wrapInlineContentLine(
|
|
|
202
218
|
}
|
|
203
219
|
};
|
|
204
220
|
|
|
205
|
-
const appendTextPart = (rawPart: string) => {
|
|
221
|
+
const appendTextPart = (token: ChatInlineToken, rawPart: string) => {
|
|
206
222
|
let part = rawPart;
|
|
207
223
|
const partWidth = displayWidth(part);
|
|
208
224
|
if (currentWidth > 0 && partWidth > safeWidth - currentWidth && partWidth <= safeWidth) {
|
|
209
225
|
finishLine();
|
|
210
226
|
part = part.trimStart();
|
|
211
227
|
}
|
|
212
|
-
appendSegment(
|
|
228
|
+
appendSegment(token, part, true);
|
|
213
229
|
};
|
|
214
230
|
|
|
215
|
-
const appendText = (
|
|
216
|
-
const parts = value.match(/\S+\s*|\s+/g) ?? [value];
|
|
231
|
+
const appendText = (token: ChatInlineToken) => {
|
|
232
|
+
const parts = token.value.match(/\S+\s*|\s+/g) ?? [token.value];
|
|
217
233
|
for (const part of parts) {
|
|
218
|
-
appendTextPart(part);
|
|
234
|
+
appendTextPart(token, part);
|
|
219
235
|
}
|
|
220
236
|
};
|
|
221
237
|
|
|
222
|
-
const appendInlineToken = (token:
|
|
238
|
+
const appendInlineToken = (token: ChatInlineToken) => {
|
|
223
239
|
const tokenWidth = inlineTokenWidth(token, catalog);
|
|
224
240
|
if (currentWidth > 0 && currentWidth + tokenWidth > safeWidth) finishLine();
|
|
225
241
|
appendSegment(token, token.value, false);
|
|
226
242
|
};
|
|
227
243
|
|
|
228
|
-
for (const token of
|
|
244
|
+
for (const token of tokens) {
|
|
229
245
|
if (token.kind === "text") {
|
|
230
|
-
appendText(token
|
|
246
|
+
appendText(token);
|
|
231
247
|
continue;
|
|
232
248
|
}
|
|
233
249
|
appendInlineToken(token);
|
|
234
250
|
}
|
|
235
251
|
|
|
236
|
-
if (current.length > 0 || lines.length === 0)
|
|
252
|
+
if (current.length > 0 || lines.length === 0) finishLine();
|
|
237
253
|
return lines;
|
|
238
254
|
}
|
|
239
255
|
|
|
256
|
+
const QUOTE_BAR: ChatInlineToken = { kind: "text", value: "\u2502 ", style: { muted: true } };
|
|
257
|
+
|
|
258
|
+
/** Each wrapped row starts with `first` or, after the first, `rest`: a list item's marker and the hang under it, a quote's bar. */
|
|
259
|
+
function withPrefix(lines: ChatBodyLine[], first: ChatInlineToken, rest: ChatInlineToken): ChatBodyLine[] {
|
|
260
|
+
return lines.map((line, index) => [index === 0 ? first : rest, ...line]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A line of a code block, as written, broken where the row ends. */
|
|
264
|
+
function codeRows(text: string, width: number): ChatBodyLine[] {
|
|
265
|
+
const value = text.replace(/\t/g, " ");
|
|
266
|
+
if (!value) return [[]];
|
|
267
|
+
return splitLongTextSegmentByDisplayWidth(value, Math.max(width, 1))
|
|
268
|
+
.map((chunk): ChatBodyLine => [{ kind: "text", value: chunk, style: { code: true } }]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A message's rows in the terminal: its markdown read line by line (a heading
|
|
273
|
+
* bold, a list item's later rows hung under its text, a quote barred on every
|
|
274
|
+
* row, a code block as written) and wrapped to `width`.
|
|
275
|
+
*/
|
|
240
276
|
export function getMessageBodyTokenLines(
|
|
241
277
|
text: string,
|
|
242
278
|
width: number,
|
|
243
279
|
catalog: Record<string, InlineTickerCatalogEntry>,
|
|
244
280
|
): ChatBodyLine[] {
|
|
245
281
|
const lines: ChatBodyLine[] = [];
|
|
246
|
-
for (const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
282
|
+
for (const line of parseMarkdownLines(text)) {
|
|
283
|
+
switch (line.kind) {
|
|
284
|
+
case "blank":
|
|
285
|
+
lines.push([]);
|
|
286
|
+
break;
|
|
287
|
+
case "code":
|
|
288
|
+
lines.push(...codeRows(line.text, width));
|
|
289
|
+
break;
|
|
290
|
+
case "heading":
|
|
291
|
+
lines.push(...wrapInlineTokens(
|
|
292
|
+
chatInlineTokens(line.text).map((token) => ({ ...token, style: { ...token.style, bold: true } })),
|
|
293
|
+
width,
|
|
294
|
+
catalog,
|
|
295
|
+
));
|
|
296
|
+
break;
|
|
297
|
+
case "quote":
|
|
298
|
+
lines.push(...withPrefix(wrapInlineTokens(chatInlineTokens(line.text), width - 2, catalog), QUOTE_BAR, QUOTE_BAR));
|
|
299
|
+
break;
|
|
300
|
+
case "list": {
|
|
301
|
+
const marker = `${" ".repeat(line.indent)}${/^\d/.test(line.marker) ? line.marker : "\u2022"} `;
|
|
302
|
+
const markerWidth = displayWidth(marker);
|
|
303
|
+
lines.push(...withPrefix(
|
|
304
|
+
wrapInlineTokens(chatInlineTokens(line.text), width - markerWidth, catalog),
|
|
305
|
+
{ kind: "text", value: marker, style: { muted: true } },
|
|
306
|
+
{ kind: "text", value: " ".repeat(markerWidth) },
|
|
307
|
+
));
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "text":
|
|
311
|
+
lines.push(...wrapInlineTokens(chatInlineTokens(line.text), width, catalog));
|
|
312
|
+
break;
|
|
250
313
|
}
|
|
251
|
-
lines.push(...wrapInlineContentLine(paragraph, width, catalog));
|
|
252
314
|
}
|
|
253
315
|
return lines.length > 0 ? lines : [[]];
|
|
254
316
|
}
|
|
@@ -267,9 +329,7 @@ export function estimateMessageHeight(
|
|
|
267
329
|
// The terminal draws a row per image and one for a review note; an image-only message has no text row.
|
|
268
330
|
const bodyLineCount = content.length === 0 && (imageCount > 0 || links.length > 0)
|
|
269
331
|
? 0
|
|
270
|
-
: catalog
|
|
271
|
-
? getMessageBodyTokenLines(content, contentLineWidth, catalog).length
|
|
272
|
-
: wrapTextLines(content, contentLineWidth).length;
|
|
332
|
+
: getMessageBodyTokenLines(content, contentLineWidth, catalog ?? {}).length;
|
|
273
333
|
const reviewNoteHeight = message.attachmentReview ? 1 : 0;
|
|
274
334
|
const reactionRows = message.reactions?.length ? terminalReactionRows(message.reactions, contentLineWidth).length : 0;
|
|
275
335
|
return headerHeight + (message.replyTo ? 1 : 0) + bodyLineCount + links.length + imageCount + reviewNoteHeight + reactionRows;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { markdownPlainText, parseInlineMarkdown, parseMarkdownLines, type MarkdownSegment } from "../../../utils/markdown";
|
|
2
|
+
import { safeExternalUrl } from "../../../utils/external-url";
|
|
3
|
+
import { tokenizeInlineContent, type InlineContentToken } from "../../../utils/inline-content-tokenizer";
|
|
4
|
+
|
|
5
|
+
/** How a run of message text is drawn. `muted` is the dim of a list marker, a quote's bar or a link's host. */
|
|
6
|
+
export interface ChatTextStyle {
|
|
7
|
+
bold?: boolean;
|
|
8
|
+
italic?: boolean;
|
|
9
|
+
strike?: boolean;
|
|
10
|
+
code?: boolean;
|
|
11
|
+
muted?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Message text, a link, a $TICKER or an @mention, with the marks around it. */
|
|
15
|
+
export type ChatInlineToken = InlineContentToken & { style?: ChatTextStyle };
|
|
16
|
+
|
|
17
|
+
/** A message link opens in the browser, so only a web address is read as one. */
|
|
18
|
+
function isChatLinkHref(href: string): boolean {
|
|
19
|
+
return safeExternalUrl(href) !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A message as one plain line's worth of text: no marks, a link as its label. */
|
|
23
|
+
export function chatPlainText(text: string): string {
|
|
24
|
+
return markdownPlainText(text, { links: isChatLinkHref });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function segmentStyle(segment: MarkdownSegment): ChatTextStyle | undefined {
|
|
28
|
+
const style: ChatTextStyle = {};
|
|
29
|
+
if (segment.bold) style.bold = true;
|
|
30
|
+
if (segment.italic) style.italic = true;
|
|
31
|
+
if (segment.strike) style.strike = true;
|
|
32
|
+
if (segment.code) style.code = true;
|
|
33
|
+
return Object.keys(style).length > 0 ? style : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function withStyle<T extends InlineContentToken>(token: T, style: ChatTextStyle | undefined): T & { style?: ChatTextStyle } {
|
|
37
|
+
return style ? { ...token, style } : token;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function linkHost(url: string): string {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether a link's label names the host it goes to, as a whole name: "x.com" does not name x.co. */
|
|
49
|
+
function labelNamesHost(label: string, host: string): boolean {
|
|
50
|
+
const text = label.toLowerCase().replace(/\bwww\./g, "");
|
|
51
|
+
for (let at = text.indexOf(host); at >= 0; at = text.indexOf(host, at + 1)) {
|
|
52
|
+
const before = text[at - 1] ?? "";
|
|
53
|
+
const after = text.slice(at + host.length);
|
|
54
|
+
if (!/[a-z0-9.-]/.test(before) && !/^(?:[a-z0-9-]|\.[a-z0-9])/.test(after)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One line of a message as tokens. A written link (`[label](url)`) shows its
|
|
61
|
+
* label, followed by the host it goes to unless the label already names it,
|
|
62
|
+
* so a label never hides where a link leads. Code is text as written: no
|
|
63
|
+
* links, mentions or tickers in it.
|
|
64
|
+
*/
|
|
65
|
+
export function chatInlineTokens(text: string): ChatInlineToken[] {
|
|
66
|
+
const segments = parseInlineMarkdown(text, { links: isChatLinkHref });
|
|
67
|
+
const tokens: ChatInlineToken[] = [];
|
|
68
|
+
let label = "";
|
|
69
|
+
segments.forEach((segment, index) => {
|
|
70
|
+
const style = segmentStyle(segment);
|
|
71
|
+
if (segment.link !== undefined) {
|
|
72
|
+
const url = safeExternalUrl(segment.link) ?? segment.link;
|
|
73
|
+
tokens.push(withStyle({ kind: "link", value: segment.text, url }, style));
|
|
74
|
+
label += segment.text;
|
|
75
|
+
// A label with marks in it is several segments of one link.
|
|
76
|
+
if (segments[index + 1]?.link === segment.link) return;
|
|
77
|
+
const host = linkHost(url);
|
|
78
|
+
if (host && !labelNamesHost(label, host)) tokens.push({ kind: "text", value: ` (${host})`, style: { muted: true } });
|
|
79
|
+
label = "";
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (segment.code) {
|
|
83
|
+
tokens.push(withStyle({ kind: "text", value: segment.text }, style));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const token of tokenizeInlineContent(segment.text)) tokens.push(withStyle(token, style));
|
|
87
|
+
});
|
|
88
|
+
return tokens;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every token a message draws outside its code blocks, in reading order. */
|
|
92
|
+
export function chatMessageTokens(text: string): ChatInlineToken[] {
|
|
93
|
+
return parseMarkdownLines(text).flatMap((line) => line.kind === "blank" || line.kind === "code" ? [] : chatInlineTokens(line.text));
|
|
94
|
+
}
|
|
@@ -12,7 +12,7 @@ import { ChatMessageActions, ChatMessageHeader, chatHeaderFitsEveryIcon } from "
|
|
|
12
12
|
import { MESSAGE_ACTION_WIDTH } from "../layout";
|
|
13
13
|
import { canReactToMessage } from "../reactions";
|
|
14
14
|
import { DesktopChatReactions, DesktopReactionAction } from "./reactions";
|
|
15
|
-
import {
|
|
15
|
+
import { DesktopChatMessageText } from "./inline-tokens";
|
|
16
16
|
import { DesktopChatPaneChip } from "./pane-chip";
|
|
17
17
|
import { chatMessagePaneLinks } from "../pane-links/model";
|
|
18
18
|
import { useSlowPendingSend } from "./pending-send";
|
|
@@ -192,10 +192,11 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
192
192
|
style={{ minWidth: 0, alignItems: "flex-start" }}
|
|
193
193
|
>
|
|
194
194
|
<Box flexGrow={1} data-gloom-role="chat-message-body" data-gloom-selectable="true" style={{ minWidth: 0 }}>
|
|
195
|
-
<
|
|
195
|
+
<DesktopChatMessageText
|
|
196
196
|
text={content}
|
|
197
197
|
catalog={catalog}
|
|
198
198
|
textColor={state.bodyColor}
|
|
199
|
+
mutedColor={state.bodyMutedColor}
|
|
199
200
|
openTicker={openTicker}
|
|
200
201
|
userByUsername={userByUsername}
|
|
201
202
|
onUserHover={onUserHover}
|