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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CorporateActionsData, DeclaredDividendAction } from "../types/financials";
|
|
2
|
+
|
|
3
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
+
/** A paid row this close to a declared one is that distribution, dated a day apart. */
|
|
5
|
+
const SAME_DISTRIBUTION_DAYS = 3;
|
|
6
|
+
const DAY_MS = 86_400_000;
|
|
7
|
+
|
|
8
|
+
function isoDate(value: unknown): string | undefined {
|
|
9
|
+
return typeof value === "string" && ISO_DATE.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`)) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The declared, unpaid distributions a corporate actions payload carries,
|
|
14
|
+
* oldest first: rows the source states with a real ex-date and amount, still
|
|
15
|
+
* owed today (not yet paid, or with no pay date, not yet ex), and not in the
|
|
16
|
+
* paid history yet. Kept apart from `dividends`, which history, yields and
|
|
17
|
+
* growth read. A server that does not send them, or a listing it has none
|
|
18
|
+
* for, gives none.
|
|
19
|
+
*/
|
|
20
|
+
export function declaredDividends(
|
|
21
|
+
actions: Pick<CorporateActionsData, "dividends" | "declaredDividends"> | null | undefined,
|
|
22
|
+
now = new Date(),
|
|
23
|
+
): DeclaredDividendAction[] {
|
|
24
|
+
if (!Array.isArray(actions?.declaredDividends)) return [];
|
|
25
|
+
const today = now.toISOString().slice(0, 10);
|
|
26
|
+
const paid = (actions.dividends ?? []).map((payment) => Date.parse(`${payment.exDate}T00:00:00Z`));
|
|
27
|
+
return actions.declaredDividends.flatMap((row): DeclaredDividendAction[] => {
|
|
28
|
+
const exDate = isoDate(row?.exDate);
|
|
29
|
+
if (!exDate || typeof row.amount !== "number" || !Number.isFinite(row.amount) || row.amount <= 0) return [];
|
|
30
|
+
const payDate = isoDate(row.payDate);
|
|
31
|
+
const declarationDate = isoDate(row.declarationDate);
|
|
32
|
+
const recordDate = isoDate(row.recordDate);
|
|
33
|
+
if ((payDate ?? exDate) < today) return [];
|
|
34
|
+
const at = Date.parse(`${exDate}T00:00:00Z`);
|
|
35
|
+
if (paid.some((time) => Math.abs(time - at) <= SAME_DISTRIBUTION_DAYS * DAY_MS)) return [];
|
|
36
|
+
return [{ exDate, amount: row.amount, ...declarationDate ? { declarationDate } : {},
|
|
37
|
+
...recordDate ? { recordDate } : {}, ...payDate ? { payDate } : {} }];
|
|
38
|
+
}).sort((left, right) => left.exDate.localeCompare(right.exDate));
|
|
39
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import type { DeclaredDividendAction } from "../types/financials";
|
|
2
|
+
import { formatDistributionAmount } from "./format";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* What a forward dividend yield is everywhere it shows (fundamentals, DVD):
|
|
3
6
|
* the indicated annual rate Gloom Cloud serves over the price, dated by the
|
|
4
7
|
* ex-date of the distribution it annualizes. While no other is announced that
|
|
5
8
|
* is the latest distribution, whose ex-date has passed. Once the next one is
|
|
6
9
|
* declared the rate annualizes it, and its ex-date is still ahead: the basis
|
|
7
|
-
* then names the declared distribution by its ex-date and pay date,
|
|
8
|
-
* rate "as of" a
|
|
10
|
+
* then names the declared distribution by its ex-date and pay date, and by its
|
|
11
|
+
* amount when Gloom Cloud states one for that ex-date, never a rate "as of" a
|
|
12
|
+
* day that has not come. The amount is only ever the stated one, never the
|
|
13
|
+
* rate divided back.
|
|
9
14
|
*/
|
|
10
15
|
const LATEST_DISTRIBUTION = "latest distribution annualized";
|
|
11
16
|
|
|
@@ -20,25 +25,43 @@ function isDeclared(exDate: string | null, now: Date): exDate is string {
|
|
|
20
25
|
return exDate != null && exDate > now.toISOString().slice(0, 10);
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
interface DeclaredBasisOptions {
|
|
29
|
+
/** The declared distributions Gloom Cloud states (`declaredDividends`); only the one on the rate's ex-date counts. */
|
|
30
|
+
declared?: readonly DeclaredDividendAction[];
|
|
31
|
+
/** The currency their amounts are in; without it no amount is named. */
|
|
32
|
+
currency?: string | null;
|
|
33
|
+
now?: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The declared row on the rate's ex-date, as `$0.98`, with its pay date; null when none is stated. */
|
|
37
|
+
function statedDeclaration(exDate: string, options: DeclaredBasisOptions): { amount: string; payDate: string | null } | null {
|
|
38
|
+
const row = options.currency ? options.declared?.find((declared) => declared.exDate === exDate) : undefined;
|
|
39
|
+
return row ? { amount: formatDistributionAmount(row.amount, options.currency!), payDate: isoDay(row.payDate) } : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
/**
|
|
24
43
|
* `latest distribution annualized, as of 2026-08-20`, or for a rate dated
|
|
25
|
-
* ahead, `annualized from the declared
|
|
26
|
-
* 2026-11-19
|
|
27
|
-
* date
|
|
44
|
+
* ahead, `annualized from the declared $0.98 payable 2026-12-10, ex
|
|
45
|
+
* 2026-11-19`, the amount and pay date when they are stated; the basis alone
|
|
46
|
+
* when the date is unknown. `payDate` is the pay date a source states beside
|
|
47
|
+
* that ex-date without a declared row (DVD's summary).
|
|
28
48
|
*/
|
|
29
49
|
export function forwardDividendBasis(
|
|
30
50
|
asOf: string | Date | null | undefined,
|
|
31
|
-
options: { payDate?: string | Date | null
|
|
51
|
+
options: DeclaredBasisOptions & { payDate?: string | Date | null } = {},
|
|
32
52
|
): string {
|
|
33
53
|
const exDate = isoDay(asOf);
|
|
34
54
|
if (!isDeclared(exDate, options.now ?? new Date())) return exDate ? `${LATEST_DISTRIBUTION}, as of ${exDate}` : LATEST_DISTRIBUTION;
|
|
35
|
-
const
|
|
36
|
-
|
|
55
|
+
const stated = statedDeclaration(exDate, options);
|
|
56
|
+
const payDate = stated?.payDate ?? isoDay(options.payDate);
|
|
57
|
+
return `annualized from the declared ${stated?.amount ?? "distribution"}${payDate ? ` payable ${payDate}` : ""}, ex ${exDate}`;
|
|
37
58
|
}
|
|
38
59
|
|
|
39
|
-
/** The same basis as a pane's short note under the figure: `as of 2026-08-20`, or `declared` beside the next ex-date it shows. */
|
|
40
|
-
export function forwardDividendNote(asOf: string | Date | null | undefined,
|
|
60
|
+
/** The same basis as a pane's short note under the figure: `as of 2026-08-20`, or `declared $0.98` beside the next ex-date it shows. */
|
|
61
|
+
export function forwardDividendNote(asOf: string | Date | null | undefined, options: DeclaredBasisOptions = {}): string | null {
|
|
41
62
|
const exDate = isoDay(asOf);
|
|
42
63
|
if (!exDate) return null;
|
|
43
|
-
|
|
64
|
+
if (!isDeclared(exDate, options.now ?? new Date())) return `as of ${exDate}`;
|
|
65
|
+
const stated = statedDeclaration(exDate, options);
|
|
66
|
+
return stated ? `declared ${stated.amount}` : "declared";
|
|
44
67
|
}
|
|
@@ -40,18 +40,29 @@ export function enterpriseValueCheck(
|
|
|
40
40
|
return fundamentals?.enterpriseValue === check.source ? check : undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Whether the provider's EV was withdrawn: retracted (`unavailableFields`) and none served in its place, by the
|
|
45
|
+
* served check or by Gloom Cloud's own validation of the source observation, which leaves no check.
|
|
46
|
+
*/
|
|
47
|
+
export function enterpriseValueWithdrawn(fundamentals: Parameters<typeof enterpriseValueCheck>[0]): boolean {
|
|
48
|
+
return fundamentals?.enterpriseValue === undefined && !!fundamentals?.unavailableFields?.includes("enterpriseValue");
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
/** "(provider)" beside an EV more than 5% from market cap + debt - cash, or "unavailable" for a withdrawn one. */
|
|
44
52
|
export function enterpriseValueLabel(fundamentals: Parameters<typeof enterpriseValueCheck>[0]): "provider" | "unavailable" | undefined {
|
|
45
53
|
const status = enterpriseValueCheck(fundamentals)?.status;
|
|
46
|
-
return status === "differs" ? "provider" : status === "withdrawn" ? "unavailable" : undefined;
|
|
54
|
+
return status === "differs" ? "provider" : status === "withdrawn" || enterpriseValueWithdrawn(fundamentals) ? "unavailable" : undefined;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
50
58
|
* The reconciliation line under a labelled or withdrawn EV: "EV 61.75B USD (provider) vs 63.75B computed from market
|
|
51
|
-
* cap + total debt - cash (balance sheet 2026-06-30)."
|
|
59
|
+
* cap + total debt - cash (balance sheet 2026-06-30)." For an EV withdrawn without a check, that the source failed
|
|
60
|
+
* validation. Undefined when the EV matches, is unchecked, or has no check. Read it from the fundamentals the rows
|
|
61
|
+
* show (atRegularSessionPrice moves the check with the EV and the market cap), so its figures are theirs.
|
|
52
62
|
*/
|
|
53
63
|
export function enterpriseValueReconciliationNote(fundamentals: Parameters<typeof enterpriseValueCheck>[0]): string | undefined {
|
|
54
64
|
const check = enterpriseValueCheck(fundamentals);
|
|
65
|
+
if (!check && enterpriseValueWithdrawn(fundamentals)) return "EV unavailable: the provider's figure failed validation.";
|
|
55
66
|
if (!check || !finiteNumber(check.computed) || (check.status !== "differs" && check.status !== "withdrawn")) return undefined;
|
|
56
67
|
const source = `${formatCompact(check.source)} ${check.currency}`;
|
|
57
68
|
const computed = formatCompact(check.computed);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown every renderer of it reads: inline marks (the reader, buildout
|
|
3
|
+
* and Ask Gloom panes through components/markdown-text.tsx, and chat), and the
|
|
4
|
+
* line structure chat draws (headings, lists, quotes and fenced code).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface MarkdownSegment {
|
|
8
|
+
text: string;
|
|
9
|
+
bold?: boolean;
|
|
10
|
+
italic?: boolean;
|
|
11
|
+
strike?: boolean;
|
|
12
|
+
code?: boolean;
|
|
13
|
+
/** Where the segment links to, when links are read. */
|
|
14
|
+
link?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InlineMarkdownOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Reads `[label](href)` as a link: every one, or those whose href this
|
|
20
|
+
* accepts. A link not read stays the text it was written as.
|
|
21
|
+
*/
|
|
22
|
+
links?: boolean | ((href: string) => boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A backslash escapes the punctuation markdown would read ("\\*" is a star).
|
|
26
|
+
// A bracket after a bang is a picture, which stays as written. A bare address
|
|
27
|
+
// is read whole, so the underscores and stars in it are not marks. A mark
|
|
28
|
+
// opens before a character and closes after one, so "2 * 3 * 4" is
|
|
29
|
+
// arithmetic, and an underscore inside a word (snake_case, a_b_c) is not one.
|
|
30
|
+
// An italic span may hold a bold one ("*covers **96%** of value*").
|
|
31
|
+
const INLINE_RE = new RegExp([
|
|
32
|
+
String.raw`\\(?<escaped>[\\` + "`" + String.raw`*_~#>[\]()-])`,
|
|
33
|
+
String.raw`(?<!!)\[(?<linkLabel>[^\]]+)\]\((?<linkHref>(?:[^()\s]|\([^()\s]*\))+)\)`,
|
|
34
|
+
String.raw`(?<url>\b(?:https?:\/\/|www\.)[^\s<>"']+)`,
|
|
35
|
+
"(?<!`)(?<ticks>`{1,3})(?!`)(?<code>[^`]|[^`].*?[^`])\\k<ticks>(?!`)",
|
|
36
|
+
String.raw`\*\*(?![\s*])(?<bold>[^*]+?)(?<!\s)\*\*`,
|
|
37
|
+
String.raw`(?<!\*)\*(?![\s*])(?<italic>(?:[^*]|\*\*[^*]+\*\*)+?)(?<!\s)\*(?!\*)`,
|
|
38
|
+
String.raw`(?<![A-Za-z0-9_])_(?![\s_])(?<underscored>[^_]+?)(?<!\s)_(?![A-Za-z0-9_])`,
|
|
39
|
+
String.raw`~~(?![\s~])(?<strike>[^~]+?)(?<!\s)~~`,
|
|
40
|
+
].join("|"), "gi");
|
|
41
|
+
|
|
42
|
+
function readsLink(options: InlineMarkdownOptions, href: string): boolean {
|
|
43
|
+
return typeof options.links === "function" ? options.links(href) : options.links === true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Code keeps its text; one space inside each tick, as in "`` `a` ``", is padding. */
|
|
47
|
+
function codeText(code: string): string {
|
|
48
|
+
return code.length > 2 && code.startsWith(" ") && code.endsWith(" ") && code.trim() ? code.slice(1, -1) : code;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseInlineMarkdown(text: string, options: InlineMarkdownOptions = {}): MarkdownSegment[] {
|
|
52
|
+
const segments: MarkdownSegment[] = [];
|
|
53
|
+
let cursor = 0;
|
|
54
|
+
const marked = (inner: string, mark: Partial<MarkdownSegment>) => {
|
|
55
|
+
for (const segment of parseInlineMarkdown(inner, options)) segments.push({ ...segment, ...mark });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const pattern = new RegExp(INLINE_RE.source, INLINE_RE.flags);
|
|
59
|
+
let match: RegExpExecArray | null;
|
|
60
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
61
|
+
const groups = match.groups ?? {};
|
|
62
|
+
if (groups.linkHref != null && !readsLink(options, groups.linkHref)) {
|
|
63
|
+
// Not read as a link: the bracket is text, and what is inside it still styles.
|
|
64
|
+
pattern.lastIndex = match.index + 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (match.index > cursor) {
|
|
68
|
+
segments.push({ text: text.slice(cursor, match.index) });
|
|
69
|
+
}
|
|
70
|
+
if (groups.escaped != null) {
|
|
71
|
+
segments.push({ text: groups.escaped });
|
|
72
|
+
} else if (groups.linkHref != null) {
|
|
73
|
+
for (const inner of parseInlineMarkdown(groups.linkLabel ?? "")) segments.push({ ...inner, link: groups.linkHref });
|
|
74
|
+
} else if (groups.url != null) {
|
|
75
|
+
segments.push({ text: groups.url });
|
|
76
|
+
} else if (groups.code != null) {
|
|
77
|
+
segments.push({ text: codeText(groups.code), code: true });
|
|
78
|
+
} else if (groups.bold != null) {
|
|
79
|
+
marked(groups.bold, { bold: true });
|
|
80
|
+
} else if (groups.italic != null) {
|
|
81
|
+
marked(groups.italic, { italic: true });
|
|
82
|
+
} else if (groups.underscored != null) {
|
|
83
|
+
marked(groups.underscored, { italic: true });
|
|
84
|
+
} else if (groups.strike != null) {
|
|
85
|
+
marked(groups.strike, { strike: true });
|
|
86
|
+
}
|
|
87
|
+
cursor = match.index + match[0].length;
|
|
88
|
+
}
|
|
89
|
+
if (cursor < text.length) {
|
|
90
|
+
segments.push({ text: text.slice(cursor) });
|
|
91
|
+
}
|
|
92
|
+
// Plain runs split by an address or an escape read as one.
|
|
93
|
+
return segments.reduce<MarkdownSegment[]>((joined, segment) => {
|
|
94
|
+
const previous = joined[joined.length - 1];
|
|
95
|
+
if (previous && isPlain(previous) && isPlain(segment)) previous.text += segment.text;
|
|
96
|
+
else joined.push({ ...segment });
|
|
97
|
+
return joined;
|
|
98
|
+
}, []);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isPlain(segment: MarkdownSegment): boolean {
|
|
102
|
+
return !segment.bold && !segment.italic && !segment.strike && !segment.code && segment.link === undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const MARKDOWN_HEADING_RE = /^(#{1,3})\s+(.+)$/;
|
|
106
|
+
|
|
107
|
+
const LIST_ITEM_RE = /^(\s*)([-*]|\d+\.)\s(.*)$/;
|
|
108
|
+
|
|
109
|
+
/** A list item's indent, marker ("-", "*" or "1.") and text, or null for any other line. */
|
|
110
|
+
export function markdownListItem(line: string): { indent: number; marker: string; text: string } | null {
|
|
111
|
+
const match = line.match(LIST_ITEM_RE);
|
|
112
|
+
return match ? { indent: match[1]!.length, marker: match[2]!, text: match[3]! } : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type MarkdownLine =
|
|
116
|
+
| { kind: "text"; text: string }
|
|
117
|
+
| { kind: "blank" }
|
|
118
|
+
| { kind: "heading"; text: string }
|
|
119
|
+
| { kind: "list"; indent: number; marker: string; text: string }
|
|
120
|
+
| { kind: "quote"; text: string }
|
|
121
|
+
/** A line inside a fenced code block, as written. */
|
|
122
|
+
| { kind: "code"; text: string };
|
|
123
|
+
|
|
124
|
+
const FENCE_OPEN_RE = /^\s{0,3}```[^`]*$/;
|
|
125
|
+
const FENCE_CLOSE_RE = /^\s{0,3}```\s*$/;
|
|
126
|
+
const QUOTE_RE = /^>(?: (.*))?$/;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A message's lines and what each is. The fence lines of a code block are
|
|
130
|
+
* dropped; a fence that never closes stays a line of text, so it cannot turn
|
|
131
|
+
* the rest of the message into code.
|
|
132
|
+
*/
|
|
133
|
+
export function parseMarkdownLines(text: string): MarkdownLine[] {
|
|
134
|
+
const lines = text.split("\n");
|
|
135
|
+
const parsed: MarkdownLine[] = [];
|
|
136
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
137
|
+
const line = lines[index]!;
|
|
138
|
+
if (FENCE_OPEN_RE.test(line)) {
|
|
139
|
+
const close = lines.findIndex((candidate, next) => next > index && FENCE_CLOSE_RE.test(candidate));
|
|
140
|
+
if (close > index) {
|
|
141
|
+
for (const code of lines.slice(index + 1, close)) parsed.push({ kind: "code", text: code });
|
|
142
|
+
index = close;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (line.trim() === "") {
|
|
147
|
+
parsed.push({ kind: "blank" });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const heading = line.match(MARKDOWN_HEADING_RE);
|
|
151
|
+
if (heading) {
|
|
152
|
+
parsed.push({ kind: "heading", text: heading[2]! });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const quote = line.match(QUOTE_RE);
|
|
156
|
+
if (quote) {
|
|
157
|
+
parsed.push({ kind: "quote", text: quote[1] ?? "" });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const item = markdownListItem(line);
|
|
161
|
+
parsed.push(item ? { kind: "list", ...item } : { kind: "text", text: line });
|
|
162
|
+
}
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The text without its marks, for wherever it shows as one plain line (a
|
|
168
|
+
* reply's quote, a notification). A link reads as its label.
|
|
169
|
+
*/
|
|
170
|
+
export function markdownPlainText(text: string, options: InlineMarkdownOptions = {}): string {
|
|
171
|
+
const plain = (line: string) => parseInlineMarkdown(line, options).map((segment) => segment.text).join("");
|
|
172
|
+
return parseMarkdownLines(text).map((line) => {
|
|
173
|
+
switch (line.kind) {
|
|
174
|
+
case "blank": return "";
|
|
175
|
+
case "code": return line.text;
|
|
176
|
+
case "list": return `${" ".repeat(line.indent)}${line.marker} ${plain(line.text)}`;
|
|
177
|
+
default: return plain(line.text);
|
|
178
|
+
}
|
|
179
|
+
}).join("\n");
|
|
180
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Fundamentals, Quote } from "../types/financials";
|
|
2
|
+
import { formatCompact, formatNumber } from "./format";
|
|
2
3
|
|
|
3
4
|
export interface MarketCapitalization {
|
|
4
5
|
value: number;
|
|
@@ -64,3 +65,24 @@ export function convertMarketCapitalization(
|
|
|
64
65
|
export function listedMarketCapitalization(value: number | null): number | null {
|
|
65
66
|
return value != null && Number.isFinite(value) && value > 0 ? value : null;
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
/** How far a market cap may sit from the share count times the price and still rest on that count. */
|
|
70
|
+
const SHARE_COUNT_TOLERANCE = 0.02;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where a market cap does not rest on the share count shown beside it (one class's count beside an all-class cap):
|
|
74
|
+
* "Market cap 137.20B USD is the provider's and does not rest on the 799.93M shares shown (x 110.11 USD = 88.08B)."
|
|
75
|
+
* Undefined when they tie, or without a count and a price in the cap's currency to test.
|
|
76
|
+
*/
|
|
77
|
+
export function marketCapShareBasisNote(
|
|
78
|
+
capitalization: Pick<MarketCapitalization, "value" | "currency"> | null,
|
|
79
|
+
shares: number | undefined,
|
|
80
|
+
price: number | null | undefined,
|
|
81
|
+
priceCurrency: string | undefined,
|
|
82
|
+
): string | undefined {
|
|
83
|
+
if (!capitalization || !(capitalization.value > 0) || capitalization.currency !== priceCurrency) return undefined;
|
|
84
|
+
if (shares == null || !Number.isFinite(shares) || !(shares > 0) || price == null || !Number.isFinite(price) || !(price > 0)) return undefined;
|
|
85
|
+
const product = shares * price;
|
|
86
|
+
if (Math.abs(capitalization.value / product - 1) <= SHARE_COUNT_TOLERANCE) return undefined;
|
|
87
|
+
return `Market cap ${formatCompact(capitalization.value)} ${capitalization.currency} is the provider's and does not rest on the ${formatCompact(shares)} shares shown (x ${formatNumber(price, price < 1 ? 4 : 2)} ${priceCurrency} = ${formatCompact(product)}).`;
|
|
88
|
+
}
|