@k2b/cloud 0.36.0 → 0.38.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/package.json +5 -3
- package/scripts/README.md +1 -0
- package/scripts/cli-pack.ts +123 -0
- package/src/_internal/help-catalog.ts +123 -4
- package/src/_internal/help.ts +27 -6
- package/src/_internal/shell-focus.ts +11 -0
- package/src/access/PermissionEditor.browser-harness.tsx +46 -0
- package/src/access/PermissionEditor.tsx +38 -9
- package/src/access/ui.ts +1 -0
- package/src/account/Avatar.tsx +20 -2
- package/src/account/ui.ts +1 -1
- package/src/ai/browser.ts +1 -0
- package/src/ai/chart-block.ts +44 -13
- package/src/ai/chat/answer-links.ts +8 -0
- package/src/ai/chat/chart-block.tsx +2 -11
- package/src/ai/chat/messages.ts +12 -0
- package/src/ai/chat/primitives.tsx +2 -1
- package/src/ai/chat/web-tools.tsx +23 -6
- package/src/ai/chat-tasks.ts +65 -1
- package/src/ai/default-tool-contracts.ts +30 -13
- package/src/ai/executor.ts +46 -3
- package/src/ai/fetch-file-tool.ts +37 -3
- package/src/ai/migrate.ts +19 -2
- package/src/ai/readable-html.ts +1057 -0
- package/src/ai/types.ts +1 -0
- package/src/api/help.ts +23 -4
- package/src/api/mcp.ts +8 -2
- package/src/browser/FileChooser.tsx +11 -3
- package/src/browser/choose-files.browser-harness.tsx +4 -1
- package/src/browser/file-chooser-messages.ts +4 -0
- package/src/browser/file-providers.ts +14 -1
- package/src/browser/provider-folder.tsx +58 -5
- package/src/browser/resource-picker.tsx +5 -1
- package/src/browser/shell-focus.ts +67 -0
- package/src/cli/admin/workflows.ts +1 -0
- package/src/cli/commands.ts +2 -0
- package/src/cli/plugin.ts +5 -5
- package/src/config/env.ts +14 -0
- package/src/contracts/file-provider.ts +48 -1
- package/src/contracts/outgoing-mail.ts +8 -1
- package/src/server/help.ts +136 -10
- package/src/server/services/geo.ts +21 -4
- package/src/services/allowed-network-fetch.ts +189 -0
- package/src/services/help/index.ts +46 -5
- package/src/services/help/store.ts +26 -5
- package/src/services/help/types.ts +10 -2
- package/src/services/index.ts +2 -0
- package/src/services/network-security.ts +28 -0
- package/src/services/outgoing-mail/dispatcher.ts +7 -2
- package/src/services/outgoing-mail/resolve-host.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +18 -3
- package/src/services/outgoing-mail/transport.ts +24 -13
- package/src/services/settings/index.ts +48 -26
- package/src/shared/help.ts +41 -0
- package/src/shared/index.ts +9 -1
- package/src/shared/markdown/extensions/guided-help.ts +208 -64
- package/src/shared/markdown/formula.ts +8 -8
- package/src/shared/markdown/index.ts +72 -11
- package/src/shared/network-address.ts +9 -0
- package/src/ssr/CloudShell.island.tsx +2 -0
- package/src/ssr/GlobalSearchTrigger.tsx +1 -1
- package/src/ssr/Layout.tsx +28 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
- package/src/ssr/LayoutHelp.tsx +569 -410
- package/src/ssr/LayoutHelpTrigger.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -1
- package/src/ssr/ProfilePreferences.tsx +1 -1
- package/src/ssr/RailApps.tsx +1 -1
- package/src/ssr/ShellFocusToggle.tsx +43 -0
- package/src/ssr/ShellRuntime.island.tsx +53 -26
- package/src/ssr/app-launchpad.tsx +1 -1
- package/src/ssr/help-messages.ts +60 -38
- package/src/ssr/help.ts +3 -0
- package/src/ssr/layout-help-article.ts +41 -0
- package/src/ssr/layout-help-navigation.ts +0 -12
- package/src/ssr/layout-help-registry.tsx +4 -11
- package/src/ssr/layout-help-search.ts +9 -12
- package/src/ssr/layout-help-start.ts +68 -0
- package/src/ssr/platform-messages.ts +14 -0
- package/src/ssr/shell-data.tsx +7 -0
- package/src/ssr/shell-focus.browser-harness.tsx +83 -0
- package/src/ssr/shell-shortcuts.ts +8 -0
- package/src/styles/effects.css +688 -185
- package/src/styles/global.css +155 -26
- package/src/styles/resource-search.css +25 -0
- package/src/styles/utilities-navigation.css +11 -3
- package/src/workflows/store/events.ts +24 -0
- package/src/workflows/store/index.ts +8 -0
- package/src/workflows/store/observability.ts +6 -1
- package/src/workflows/store/retention.ts +157 -0
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Lexer, type MarkedExtension, type Token, type Tokens } from "marked";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const guidedBlocks = new Set<GuidedBlock>(["compare", "reference", "steps"]);
|
|
6
|
-
const headingMetaPattern = /\s+\{icon="([a-z0-9-]+)"\}\s*$/i;
|
|
7
|
-
|
|
8
|
-
const slug = (value: string): string =>
|
|
3
|
+
export const headingMetaPattern = /\s+\{icon="([a-z0-9-]+)"\}\s*$/i;
|
|
4
|
+
export const helpHeadingSlug = (value: string): string =>
|
|
9
5
|
value
|
|
10
6
|
.toLocaleLowerCase()
|
|
11
7
|
.normalize("NFKD")
|
|
@@ -13,63 +9,218 @@ const slug = (value: string): string =>
|
|
|
13
9
|
.replace(/[^a-z0-9]+/g, "-")
|
|
14
10
|
.replace(/^-+|-+$/g, "") || "section";
|
|
15
11
|
|
|
16
|
-
const headingTitleTokens = (token: Tokens.Heading
|
|
17
|
-
if (!hasMetadata) return token.tokens;
|
|
18
|
-
|
|
12
|
+
const headingTitleTokens = (token: Tokens.Heading): Token[] => {
|
|
19
13
|
const tokens = [...token.tokens];
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
14
|
+
const trailing = tokens.at(-1);
|
|
15
|
+
if (trailing?.type === "text")
|
|
16
|
+
tokens[tokens.length - 1] = {
|
|
17
|
+
...trailing,
|
|
18
|
+
raw: trailing.raw.replace(headingMetaPattern, ""),
|
|
19
|
+
text: trailing.text.replace(headingMetaPattern, ""),
|
|
20
|
+
};
|
|
21
|
+
return tokens;
|
|
22
|
+
};
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
24
|
+
const FENCE = /^([ \t]*(?:>[ \t]*|(?:[-+*]|\d+[.)])[ \t]+)*)(`{3,}|~{3,})(.*)$/;
|
|
25
|
+
// A directive name ends at a space or the line end, as in the openers themselves: `:::reference-data` is text.
|
|
26
|
+
const DIRECTIVE_OPENER = /^ {0,3}:::(?:steps|reference|compare|details|note|info|success|warning|danger)(?=[ \t]|$)/i;
|
|
27
|
+
const DIRECTIVE_CLOSER = /^ {0,3}:::[ \t]*$/;
|
|
28
|
+
const CALLOUT_OPENER = /^ {0,3}:::(?:note|info|success|warning|danger)(?=[ \t]|$)/i;
|
|
29
|
+
const DETAILS_OPENER = /^ {0,3}:::details(?=[ \t]|$)/i;
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const detailsLexers = new WeakSet<Lexer>();
|
|
32
|
+
|
|
33
|
+
/** The indentation column of a line prefix; a tab advances to the next multiple of four, as in Markdown. */
|
|
34
|
+
const columns = (prefix: string): number => {
|
|
35
|
+
let column = 0;
|
|
36
|
+
for (const char of prefix) column = char === "\t" ? column + 4 - (column % 4) : column + 1;
|
|
37
|
+
return column;
|
|
32
38
|
};
|
|
39
|
+
/** The indentation after a prefix's last quote marker. */
|
|
40
|
+
const indentAfterQuotes = (prefix: string): number => columns(prefix.slice(prefix.lastIndexOf(">") + 1));
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
43
|
+
* A `:::details Title` block up to its own closing `:::`. One directive inside it (steps, reference, compare, a
|
|
44
|
+
* callout, which stays text there) keeps its own closer; directives do not nest further, and fenced code hides
|
|
45
|
+
* its lines. Details never nest; another details opener or a missing closer leaves the first opener as text.
|
|
37
46
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const detailsBlock = (
|
|
48
|
+
source: string,
|
|
49
|
+
start = 0,
|
|
50
|
+
): { block?: { raw: string; title: string; body: string; callout: boolean }; stoppedAt: number } => {
|
|
51
|
+
const openerEnd = source.indexOf("\n", start);
|
|
52
|
+
const opener = /^:::details[ \t]+(\S[^\n]*)\n/.exec(source.slice(start, openerEnd + 1));
|
|
53
|
+
if (!opener) return { stoppedAt: source.length };
|
|
54
|
+
let offset = start + opener[0].length;
|
|
55
|
+
let inner = false;
|
|
56
|
+
let callout = false;
|
|
57
|
+
let listContentColumn: number | undefined;
|
|
58
|
+
let fence: { marker: string; prefix: string; quotes: number; list: boolean } | undefined;
|
|
59
|
+
while (offset < source.length) {
|
|
60
|
+
const newline = source.indexOf("\n", offset);
|
|
61
|
+
const end = newline < 0 ? source.length : newline + 1;
|
|
62
|
+
const line = source.slice(offset, end);
|
|
63
|
+
const text = line.replace(/\r?\n$/, "");
|
|
64
|
+
const marker = FENCE.exec(text);
|
|
65
|
+
const prefix = /^[ \t]*(?:>[ \t]*|(?:[-+*]|\d+[.)])[ \t]+)*/.exec(text)![0];
|
|
66
|
+
const quotes = prefix.split(">").length - 1;
|
|
67
|
+
const list = /[-+*]|\d+[.)]/.test(prefix);
|
|
68
|
+
const indentation = indentAfterQuotes(prefix);
|
|
69
|
+
if (fence && (quotes < fence.quotes || (fence.list && text.trim() && columns(/^[ \t>]*/.exec(text)![0]) < columns(fence.prefix))))
|
|
70
|
+
fence = undefined;
|
|
71
|
+
if (!fence) {
|
|
72
|
+
if (list) listContentColumn = indentation;
|
|
73
|
+
else if (text.trim() && listContentColumn !== undefined && indentation < listContentColumn) listContentColumn = undefined;
|
|
74
|
+
}
|
|
75
|
+
if (fence) {
|
|
76
|
+
if (
|
|
77
|
+
marker &&
|
|
78
|
+
marker[2]![0] === fence.marker[0] &&
|
|
79
|
+
marker[2]!.length >= fence.marker.length &&
|
|
80
|
+
/^[ \t]*$/.test(marker[3]!) &&
|
|
81
|
+
quotes === fence.quotes &&
|
|
82
|
+
indentAfterQuotes(marker[1]!) <= indentAfterQuotes(fence.prefix) + 3
|
|
83
|
+
)
|
|
84
|
+
fence = undefined;
|
|
85
|
+
} else if (marker && indentation - (listContentColumn ?? 0) <= 3 && !(marker[2]![0] === "`" && marker[3]!.includes("`")))
|
|
86
|
+
fence = { marker: marker[2]!, prefix: marker[1]!, quotes, list };
|
|
87
|
+
else if (DETAILS_OPENER.test(text)) return { stoppedAt: offset };
|
|
88
|
+
else if (DIRECTIVE_CLOSER.test(text)) {
|
|
89
|
+
if (!inner)
|
|
90
|
+
return {
|
|
91
|
+
block: {
|
|
92
|
+
raw: source.slice(start, end),
|
|
93
|
+
title: opener[1]!.trim(),
|
|
94
|
+
body: source.slice(start + opener[0].length, offset),
|
|
95
|
+
callout,
|
|
96
|
+
},
|
|
97
|
+
stoppedAt: end,
|
|
98
|
+
};
|
|
99
|
+
inner = false;
|
|
100
|
+
} else if (DIRECTIVE_OPENER.test(text)) {
|
|
101
|
+
inner = true;
|
|
102
|
+
callout ||= CALLOUT_OPENER.test(text);
|
|
103
|
+
}
|
|
104
|
+
offset = end;
|
|
105
|
+
}
|
|
106
|
+
return { stoppedAt: source.length };
|
|
107
|
+
};
|
|
40
108
|
|
|
109
|
+
const menuSegments = (tokens: Token[]): Token[][] => {
|
|
110
|
+
const segments: Token[][] = [[]];
|
|
111
|
+
for (const token of tokens) {
|
|
112
|
+
if (token.type !== "text" || !token.text.includes(" → ")) {
|
|
113
|
+
segments.at(-1)!.push(token);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const parts = token.text.split(" → ");
|
|
117
|
+
for (const [index, text] of parts.entries()) {
|
|
118
|
+
if (index > 0) segments.push([]);
|
|
119
|
+
segments.at(-1)!.push({ type: "text", raw: text, text });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return segments;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Help-only syntax; per-reader keys, links and heading ids are assigned after lexing. */
|
|
126
|
+
export function guidedHelpExtension(): MarkedExtension {
|
|
41
127
|
return {
|
|
42
|
-
hooks: {
|
|
43
|
-
preprocess(source) {
|
|
44
|
-
headingIds.clear();
|
|
45
|
-
return source;
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
128
|
renderer: {
|
|
49
|
-
paragraph(token
|
|
50
|
-
if (token.tokens.length !== 1 || token.tokens[0]?.type !== "strong") return false;
|
|
51
|
-
return `<p class="help-eyebrow">${this.parser.parseInline(token.tokens)}</p>`;
|
|
129
|
+
paragraph(token) {
|
|
130
|
+
if (token.tokens.length !== 1 || token.tokens[0]?.type !== "strong" || token.tokens[0].text.includes(" → ")) return false;
|
|
131
|
+
return `<p class="help-eyebrow">${this.parser.parseInline(token.tokens ?? [])}</p>`;
|
|
52
132
|
},
|
|
53
|
-
heading(token
|
|
54
|
-
const
|
|
55
|
-
const title =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<span class="help-section-icon" aria-hidden="true"><i class="${icon}"></i></span>
|
|
65
|
-
<span>${this.parser.parseInline(headingTitleTokens(token, Boolean(metadata)))}</span>
|
|
66
|
-
</h2>`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return `<h${token.depth} id="${id}">${this.parser.parseInline(token.tokens)}</h${token.depth}>`;
|
|
133
|
+
heading(token) {
|
|
134
|
+
const id: unknown = Reflect.get(token, "helpId");
|
|
135
|
+
const title = this.parser.parseInline(token.depth === 2 ? headingTitleTokens(token) : token.tokens);
|
|
136
|
+
return `<h${token.depth} id="${typeof id === "string" ? id : "section"}"${token.depth === 2 ? ' class="help-section-title"' : ""}>${title}</h${token.depth}>`;
|
|
137
|
+
},
|
|
138
|
+
strong(token) {
|
|
139
|
+
if (!token.text.includes(" → ")) return false;
|
|
140
|
+
const chips = menuSegments(token.tokens ?? []).map(
|
|
141
|
+
(segment) => `<span class="k2b-label-chip">${this.parser.parseInline(segment)}</span>`,
|
|
142
|
+
);
|
|
143
|
+
return `<span class="help-menu-path">${chips.join('<span class="help-menu-path-separator" aria-hidden="true">›</span><span class="k2b-sr-only"> → </span>')}</span>`;
|
|
70
144
|
},
|
|
71
145
|
},
|
|
72
146
|
extensions: [
|
|
147
|
+
{
|
|
148
|
+
name: "helpKbd",
|
|
149
|
+
level: "inline",
|
|
150
|
+
start: (source) => source.indexOf(":kbd["),
|
|
151
|
+
tokenizer(source) {
|
|
152
|
+
const match = /^:kbd\[([^\]\n]*)\]/.exec(source);
|
|
153
|
+
return match ? { type: "helpKbd", raw: match[0], keys: match[1] } : undefined;
|
|
154
|
+
},
|
|
155
|
+
renderer: (token) => String(token.html ?? ""),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "helpLabel",
|
|
159
|
+
level: "inline",
|
|
160
|
+
start: (source) => source.indexOf("**"),
|
|
161
|
+
tokenizer(source) {
|
|
162
|
+
const match = /^(\*\*(?:[^*]|\*(?!\*))+?\*\*)\{icon="([a-z0-9-]+)"\}/.exec(source);
|
|
163
|
+
if (!match) return undefined;
|
|
164
|
+
const [strong] = Lexer.lexInline(match[1]!);
|
|
165
|
+
if (strong?.type !== "strong") return undefined;
|
|
166
|
+
const icon = /^\{icon="([a-z0-9-]+)"\}/.exec(source.slice(strong.raw.length));
|
|
167
|
+
if (!icon) return undefined;
|
|
168
|
+
return {
|
|
169
|
+
...strong,
|
|
170
|
+
type: "helpLabel",
|
|
171
|
+
raw: strong.raw + icon[0],
|
|
172
|
+
icon: icon[1],
|
|
173
|
+
tokens: this.lexer.inlineTokens(strong.text),
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
renderer(token) {
|
|
177
|
+
const icon = `<i class="ti ti-${token.icon}" aria-hidden="true"></i>`;
|
|
178
|
+
const segments = menuSegments(token.tokens ?? []);
|
|
179
|
+
const chips = segments.map(
|
|
180
|
+
(segment, index) => `<span class="k2b-label-chip">${index === 0 ? icon : ""}${this.parser.parseInline(segment)}</span>`,
|
|
181
|
+
);
|
|
182
|
+
return segments.length === 1
|
|
183
|
+
? chips[0]!
|
|
184
|
+
: `<span class="help-menu-path">${chips.join('<span class="help-menu-path-separator" aria-hidden="true">›</span><span class="k2b-sr-only"> → </span>')}</span>`;
|
|
185
|
+
},
|
|
186
|
+
childTokens: ["tokens"],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "helpUnresolvedLink",
|
|
190
|
+
level: "inline",
|
|
191
|
+
renderer(token) {
|
|
192
|
+
return this.parser.parseInline(token.tokens ?? []);
|
|
193
|
+
},
|
|
194
|
+
childTokens: ["tokens"],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "helpDetails",
|
|
198
|
+
level: "block",
|
|
199
|
+
tokenizer(source) {
|
|
200
|
+
if (detailsLexers.has(this.lexer)) return undefined;
|
|
201
|
+
const { block } = detailsBlock(source);
|
|
202
|
+
if (!block) return undefined;
|
|
203
|
+
detailsLexers.add(this.lexer);
|
|
204
|
+
try {
|
|
205
|
+
return {
|
|
206
|
+
type: "helpDetails",
|
|
207
|
+
raw: block.raw,
|
|
208
|
+
title: block.title,
|
|
209
|
+
// Queued like paragraph text, so a link definition below the block applies to the title too.
|
|
210
|
+
titleTokens: this.lexer.inline(block.title),
|
|
211
|
+
callout: block.callout,
|
|
212
|
+
// The body joins this document: its links, steps, and reference blocks work as anywhere else.
|
|
213
|
+
tokens: this.lexer.blockTokens(block.body, []),
|
|
214
|
+
};
|
|
215
|
+
} finally {
|
|
216
|
+
detailsLexers.delete(this.lexer);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
renderer(token) {
|
|
220
|
+
return `<details class="k2b-disclosure help-details" data-surface="plain"><summary><span>${this.parser.parseInline(token.titleTokens ?? [])}</span><i class="ti ti-chevron-down k2b-disclosure__chevron" aria-hidden="true"></i></summary><div class="k2b-disclosure__content">${this.parser.parse(token.tokens ?? [])}</div></details>`;
|
|
221
|
+
},
|
|
222
|
+
childTokens: ["titleTokens", "tokens"],
|
|
223
|
+
},
|
|
73
224
|
{
|
|
74
225
|
name: "guidedHelpBlock",
|
|
75
226
|
level: "block",
|
|
@@ -77,21 +228,14 @@ export function guidedHelpExtension(): MarkedExtension {
|
|
|
77
228
|
return source.match(/^:::(?:compare|reference|steps)\b/)?.index;
|
|
78
229
|
},
|
|
79
230
|
tokenizer(source) {
|
|
80
|
-
const match =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const content = match[2]?.trim() ?? "";
|
|
85
|
-
return {
|
|
86
|
-
type: "guidedHelpBlock",
|
|
87
|
-
raw: match[0],
|
|
88
|
-
kind,
|
|
89
|
-
tokens: this.lexer.blockTokens(content),
|
|
90
|
-
};
|
|
231
|
+
const match = /^:::(compare|reference|steps)[ \t]*\n([\s\S]*?)\n:::(?:\n|$)/.exec(source);
|
|
232
|
+
return match
|
|
233
|
+
? { type: "guidedHelpBlock", raw: match[0], kind: match[1], tokens: this.lexer.blockTokens(match[2]?.trim() ?? "") }
|
|
234
|
+
: undefined;
|
|
91
235
|
},
|
|
92
|
-
renderer(token
|
|
93
|
-
const
|
|
94
|
-
return `<div class="help-${kind}">${
|
|
236
|
+
renderer(token) {
|
|
237
|
+
const body = this.parser.parse(token.tokens ?? []);
|
|
238
|
+
return `<div class="help-${token.kind}">${token.kind === "steps" ? body.replace(/<ol([^>]*) class="[^"]*"/, '<ol$1 class="k2b-steps"') : body}</div>`;
|
|
95
239
|
},
|
|
96
240
|
childTokens: ["tokens"],
|
|
97
241
|
},
|
|
@@ -571,7 +571,7 @@ const isCurrentFormulaCell = (row: number, col: number, ctx: EvalContext): boole
|
|
|
571
571
|
* a column that's itself filled with `=hours * rate` formulas computes
|
|
572
572
|
* the chain correctly instead of seeing the literal formula strings.
|
|
573
573
|
*/
|
|
574
|
-
const
|
|
574
|
+
export const evaluateTableCell = (row: number, col: number, ctx: EvalContext): EvalResult => {
|
|
575
575
|
const cell = cellAt(row, col, ctx);
|
|
576
576
|
if (!cell.startsWith("=")) {
|
|
577
577
|
const text = cell.trim();
|
|
@@ -623,7 +623,7 @@ const collectColumnValues = (arg: AST, state: EvalState): { values: string[] } |
|
|
|
623
623
|
const values: string[] = [];
|
|
624
624
|
for (let r = 0; r < state.ctx.rows.length; r++) {
|
|
625
625
|
if (isCurrentFormulaCell(r, lookup.index, state.ctx)) continue;
|
|
626
|
-
const result =
|
|
626
|
+
const result = evaluateTableCell(r, lookup.index, state.ctx);
|
|
627
627
|
if (result.kind === "ok") values.push(toString(result.value));
|
|
628
628
|
// errored cells silently skipped — same handling as non-numeric cells
|
|
629
629
|
}
|
|
@@ -790,10 +790,10 @@ const FUNCTIONS: Record<string, FuncImpl> = {
|
|
|
790
790
|
let total = 0;
|
|
791
791
|
for (let r = 0; r < state.ctx.rows.length; r++) {
|
|
792
792
|
if (isCurrentFormulaCell(r, condLookup.index, state.ctx) || isCurrentFormulaCell(r, sumLookup.index, state.ctx)) continue;
|
|
793
|
-
const condResult =
|
|
793
|
+
const condResult = evaluateTableCell(r, condLookup.index, state.ctx);
|
|
794
794
|
if (condResult.kind !== "ok") continue;
|
|
795
795
|
if (!isEqual(condResult.value, condValueArg.value)) continue;
|
|
796
|
-
const sumResult =
|
|
796
|
+
const sumResult = evaluateTableCell(r, sumLookup.index, state.ctx);
|
|
797
797
|
if (sumResult.kind !== "ok") continue;
|
|
798
798
|
const n = toNumber(sumResult.value);
|
|
799
799
|
if (n !== null) total += n;
|
|
@@ -841,7 +841,7 @@ const FUNCTIONS: Record<string, FuncImpl> = {
|
|
|
841
841
|
let sum = 0;
|
|
842
842
|
for (let i = 0; i < row.length; i++) {
|
|
843
843
|
if (i === state.ctx.currentCol) continue;
|
|
844
|
-
const result =
|
|
844
|
+
const result = evaluateTableCell(state.ctx.currentRow, i, state.ctx);
|
|
845
845
|
if (result.kind !== "ok") continue;
|
|
846
846
|
const n = toNumber(result.value);
|
|
847
847
|
if (n !== null) sum += n;
|
|
@@ -855,7 +855,7 @@ const FUNCTIONS: Record<string, FuncImpl> = {
|
|
|
855
855
|
let count = 0;
|
|
856
856
|
for (let i = 0; i < row.length; i++) {
|
|
857
857
|
if (i === state.ctx.currentCol) continue;
|
|
858
|
-
const result =
|
|
858
|
+
const result = evaluateTableCell(state.ctx.currentRow, i, state.ctx);
|
|
859
859
|
if (result.kind !== "ok") continue;
|
|
860
860
|
const n = toNumber(result.value);
|
|
861
861
|
if (n !== null) {
|
|
@@ -890,7 +890,7 @@ const FUNCTIONS: Record<string, FuncImpl> = {
|
|
|
890
890
|
}
|
|
891
891
|
const rawCell = cellAt(state.ctx.currentRow, lookup.index, state.ctx);
|
|
892
892
|
if (rawCell.trim().length === 0) return state.evaluate(args[1]!);
|
|
893
|
-
return
|
|
893
|
+
return evaluateTableCell(state.ctx.currentRow, lookup.index, state.ctx);
|
|
894
894
|
}
|
|
895
895
|
const v = state.evaluate(args[0]!);
|
|
896
896
|
if (v.kind === "error") return v;
|
|
@@ -1182,7 +1182,7 @@ const evaluateAst = (ast: AST, ctx: EvalContext): EvalResult => {
|
|
|
1182
1182
|
lookup.suggestion,
|
|
1183
1183
|
);
|
|
1184
1184
|
}
|
|
1185
|
-
return
|
|
1185
|
+
return evaluateTableCell(ctx.currentRow, lookup.index, ctx);
|
|
1186
1186
|
}
|
|
1187
1187
|
case "neg": {
|
|
1188
1188
|
const v = evaluateAst(ast.operand, ctx);
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* the visual appearance of the CodeMirror editor extensions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { markdownInfoBlocks } from "@k2b/ui";
|
|
9
|
-
import { Marked, type MarkedExtension } from "marked";
|
|
8
|
+
import { formatShortcut, type KeyboardPlatform, markdownInfoBlocks } from "@k2b/ui";
|
|
9
|
+
import { Marked, type MarkedExtension, type Token } from "marked";
|
|
10
10
|
import sanitizeHtml from "sanitize-html";
|
|
11
|
+
import { type HelpLinkResolver, parseHelpReference } from "../help";
|
|
11
12
|
import { markdownCalloutSanitizeOptions } from "./callout-sanitizer";
|
|
12
13
|
import { markdownClient } from "./client";
|
|
13
14
|
import { codeExtension, plainCodeExtension } from "./extensions/code";
|
|
14
|
-
import { guidedHelpExtension } from "./extensions/guided-help";
|
|
15
|
+
import { guidedHelpExtension, headingMetaPattern, helpHeadingSlug } from "./extensions/guided-help";
|
|
15
16
|
import { imagesExtension } from "./extensions/images";
|
|
16
17
|
import { katexExtension } from "./extensions/katex";
|
|
17
18
|
import { linksExtension } from "./extensions/links";
|
|
@@ -19,6 +20,7 @@ import { markExtension } from "./extensions/mark";
|
|
|
19
20
|
import { subSupExtension } from "./extensions/sub-sup";
|
|
20
21
|
import { tablesExtension } from "./extensions/tables";
|
|
21
22
|
import { taskListExtension } from "./extensions/task-list";
|
|
23
|
+
import { escapeHtml } from "./shared";
|
|
22
24
|
|
|
23
25
|
// Create a configured marked instance
|
|
24
26
|
type MarkdownProfile = "content" | "help";
|
|
@@ -74,7 +76,7 @@ export type MarkdownRenderOptions = {
|
|
|
74
76
|
locale?: string;
|
|
75
77
|
};
|
|
76
78
|
|
|
77
|
-
const sanitizeRenderedHtml = (html: string): string => {
|
|
79
|
+
const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"): string => {
|
|
78
80
|
const callouts = markdownCalloutSanitizeOptions();
|
|
79
81
|
return sanitizeHtml(html, {
|
|
80
82
|
...callouts,
|
|
@@ -84,6 +86,7 @@ const sanitizeRenderedHtml = (html: string): string => {
|
|
|
84
86
|
"aside",
|
|
85
87
|
"br",
|
|
86
88
|
"div",
|
|
89
|
+
...(profile === "help" ? ["details", "summary", "kbd"] : []),
|
|
87
90
|
"figcaption",
|
|
88
91
|
"figure",
|
|
89
92
|
"i",
|
|
@@ -115,7 +118,7 @@ const sanitizeRenderedHtml = (html: string): string => {
|
|
|
115
118
|
"tr",
|
|
116
119
|
],
|
|
117
120
|
allowedAttributes: {
|
|
118
|
-
"*": ["aria-hidden", "aria-label", "class", "data-
|
|
121
|
+
"*": ["aria-hidden", "aria-label", "class", "data-tone", "id", "title"],
|
|
119
122
|
a: [
|
|
120
123
|
"href",
|
|
121
124
|
"name",
|
|
@@ -128,6 +131,7 @@ const sanitizeRenderedHtml = (html: string): string => {
|
|
|
128
131
|
annotation: ["encoding"],
|
|
129
132
|
aside: [{ name: "role", multiple: false, values: ["note"] }],
|
|
130
133
|
code: ["class"],
|
|
134
|
+
...(profile === "help" ? { details: [{ name: "data-surface", multiple: false, values: ["plain"] }] } : {}),
|
|
131
135
|
div: ["class", "data-block-name", "style"],
|
|
132
136
|
img: ["alt", "class", "height", "loading", "src", "title", "width", "style"],
|
|
133
137
|
input: ["checked", "class", "disabled", "type"],
|
|
@@ -210,16 +214,73 @@ export function renderMarkdownSync(content: string, options: MarkdownRenderOptio
|
|
|
210
214
|
* Render trusted documentation Markdown with guided sections and internal
|
|
211
215
|
* navigation. All fenced code remains visible source, as in ordinary Markdown.
|
|
212
216
|
*/
|
|
213
|
-
export
|
|
217
|
+
export type HelpMarkdownRenderOptions = { locale?: string; platform?: KeyboardPlatform; links?: HelpLinkResolver };
|
|
218
|
+
export type { HelpLinkResolver } from "../help";
|
|
219
|
+
|
|
220
|
+
/** Lexer shared by rendering, startup validation and agent source conversion. */
|
|
221
|
+
export const lexHelpMarkdown = (content: string, locale?: string) => markedFor("help", "widget", locale).lexer(content);
|
|
222
|
+
/**
|
|
223
|
+
* Visit tokens in the order of marked's walkTokens. Marked concatenates every visitor result into a new array per
|
|
224
|
+
* token, which grows quadratically with the number of sibling tokens in one paragraph.
|
|
225
|
+
*/
|
|
226
|
+
export const walkHelpTokens = (tokens: Token[], visitor: (token: Token) => void) => {
|
|
227
|
+
const childTokens = markedFor("help").defaults.extensions?.childTokens ?? {};
|
|
228
|
+
const walk = (list: readonly Token[]) => {
|
|
229
|
+
for (const token of list) {
|
|
230
|
+
visitor(token);
|
|
231
|
+
if (token.type === "table") {
|
|
232
|
+
for (const cell of token.header) walk(cell.tokens);
|
|
233
|
+
for (const row of token.rows) for (const cell of row) walk(cell.tokens);
|
|
234
|
+
} else if (token.type === "list") walk(token.items);
|
|
235
|
+
else if (childTokens[token.type])
|
|
236
|
+
for (const key of childTokens[token.type]!) walk((Reflect.get(token, key) as Token[] | Token[][]).flat(Infinity) as Token[]);
|
|
237
|
+
else if ("tokens" in token && token.tokens) walk(token.tokens);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
walk(tokens);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export function renderHelpMarkdown(content: string, options: HelpMarkdownRenderOptions = {}): string {
|
|
214
244
|
if (!content || typeof content !== "string") return "";
|
|
215
|
-
const
|
|
216
|
-
|
|
245
|
+
const instance = markedFor("help", "widget", options.locale);
|
|
246
|
+
const tokens = instance.lexer(content);
|
|
247
|
+
const headingIds = new Map<string, number>();
|
|
248
|
+
walkHelpTokens(tokens, (token) => {
|
|
249
|
+
if (token.type === "heading") {
|
|
250
|
+
const base = helpHeadingSlug(token.text.replace(headingMetaPattern, "").trim());
|
|
251
|
+
const occurrence = headingIds.get(base) ?? 0;
|
|
252
|
+
headingIds.set(base, occurrence + 1);
|
|
253
|
+
Reflect.set(token, "helpId", occurrence === 0 ? base : `${base}-${occurrence + 1}`);
|
|
254
|
+
} else if (token.type === "helpKbd") {
|
|
255
|
+
const shortcut = formatShortcut(String(token.keys), { platform: options.platform ?? "other", locale: options.locale });
|
|
256
|
+
token.html = `<kbd class="k2b-kbd"><span class="k2b-sr-only">${escapeHtml(shortcut.name)}</span><span class="k2b-kbd__keys" aria-hidden="true">${shortcut.keys.map((key) => `<kbd class="k2b-kbd__key">${escapeHtml(key.label)}</kbd>`).join("")}</span></kbd>`;
|
|
257
|
+
} else if (token.type === "link" && token.href.startsWith("help:")) {
|
|
258
|
+
const ref = parseHelpReference(token.href.slice(5));
|
|
259
|
+
const target = ref && options.links?.(ref);
|
|
260
|
+
if (target) token.href = target.url + ref.fragment;
|
|
261
|
+
else token.type = "helpUnresolvedLink";
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return sanitizeRenderedHtml(instance.parser(tokens), "help");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Derive Help search text from already-rendered HTML. */
|
|
268
|
+
export function helpHtmlToPlainText(html: string): string {
|
|
269
|
+
// The text a screen reader reads: key names instead of key caps, no decorative separators, and words of
|
|
270
|
+
// adjacent blocks (a callout's title and body, a summary and its details) kept apart.
|
|
271
|
+
const spaced = html.replace(/<\/(?:p|div|summary|li|h[1-6]|td|th)>/g, "$& ");
|
|
272
|
+
// Hidden parts are spans and icons; the filter only sees tags that the first pass keeps.
|
|
273
|
+
const spoken = sanitizeHtml(spaced, {
|
|
274
|
+
allowedTags: ["span", "i"],
|
|
275
|
+
allowedAttributes: { "*": ["aria-hidden"] },
|
|
276
|
+
exclusiveFilter: (frame) => frame.attribs["aria-hidden"] === "true",
|
|
277
|
+
});
|
|
278
|
+
return sanitizeHtml(spoken, { allowedTags: [], allowedAttributes: {} }).replace(/\s+/g, " ").trim();
|
|
217
279
|
}
|
|
218
280
|
|
|
219
281
|
/** Plain text used by lightweight client-side help search indexes. */
|
|
220
|
-
export function markdownToPlainText(content: string): string {
|
|
221
|
-
|
|
222
|
-
return sanitizeHtml(html, { allowedTags: [], allowedAttributes: {} }).replace(/\s+/g, " ").trim();
|
|
282
|
+
export function markdownToPlainText(content: string, locale?: string): string {
|
|
283
|
+
return helpHtmlToPlainText(renderHelpMarkdown(content, { locale }));
|
|
223
284
|
}
|
|
224
285
|
|
|
225
286
|
export { marked };
|
|
@@ -163,6 +163,15 @@ export const parseNetworkAllowlist = (entries: readonly string[]): NetworkAllowl
|
|
|
163
163
|
return { hosts, networks };
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
+
/** Parse operator configuration with the variable name in startup errors. */
|
|
167
|
+
export const parseNetworkAllowlistEnv = (variable: string, entries: readonly string[]): NetworkAllowlist => {
|
|
168
|
+
try {
|
|
169
|
+
return parseNetworkAllowlist(entries);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new Error(`${variable}: ${error instanceof Error ? error.message : "Invalid allowlist"}`);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
166
175
|
export const isAllowlistedNetworkAddress = (allowlist: NetworkAllowlist, rawAddress: string): boolean => {
|
|
167
176
|
const literal = parseNetworkAddress(rawAddress);
|
|
168
177
|
if (!literal) return false;
|
|
@@ -9,6 +9,7 @@ import type { LayoutBreadcrumb } from "./layout-runtime";
|
|
|
9
9
|
import ProfilePreferences from "./ProfilePreferences";
|
|
10
10
|
import { platformMessages } from "./platform-messages";
|
|
11
11
|
import RailApps from "./RailApps";
|
|
12
|
+
import ShellFocusToggle from "./ShellFocusToggle";
|
|
12
13
|
import { useShellData } from "./shell-data";
|
|
13
14
|
|
|
14
15
|
type CloudShellProps =
|
|
@@ -32,6 +33,7 @@ function RailNavigation(props: { currentPath: string }) {
|
|
|
32
33
|
createEffect(() => setAppLaunchpadContext(data.apps, data.legalLinks, profile));
|
|
33
34
|
return (
|
|
34
35
|
<nav class="layout-rail-navigation flex min-h-0 flex-1 flex-col items-center gap-1" aria-label={t().apps}>
|
|
36
|
+
<ShellFocusToggle variant="rail" />
|
|
35
37
|
<RailApps apps={data.apps} settings={data.rail} currentPath={props.currentPath} />
|
|
36
38
|
<AppLaunchpadButton profile={profile} apps={data.apps} legalLinks={data.legalLinks} variant="rail" label={t().openApps} />
|
|
37
39
|
<div class="mt-auto shrink-0 flex flex-col items-center gap-1">
|
|
@@ -33,7 +33,7 @@ export default function GlobalSearchTrigger(props: GlobalSearchTriggerProps) {
|
|
|
33
33
|
delay={0}
|
|
34
34
|
content={t().searchShortcut}
|
|
35
35
|
>
|
|
36
|
-
<i class={`${action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-search"} text-
|
|
36
|
+
<i class={`${action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-search"} text-xl`} />
|
|
37
37
|
</Tooltip.Trigger>
|
|
38
38
|
);
|
|
39
39
|
|