@valentinkolb/cloud 0.4.0 → 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/package.json +18 -6
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +53 -46
- package/src/api/accounts-entities.ts +4 -0
- package/src/api/admin-core-settings.ts +98 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/auth/schemas.ts +24 -0
- package/src/api/auth.ts +113 -10
- package/src/api/index.ts +7 -2
- package/src/api/me.ts +203 -14
- package/src/api/search/schemas.ts +1 -0
- package/src/api/search.ts +62 -8
- package/src/config/ssr.ts +2 -9
- package/src/contracts/announcements.test.ts +37 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +108 -1
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/server/api/index.ts +1 -1
- package/src/server/api/respond.ts +50 -10
- package/src/server/index.ts +44 -38
- package/src/server/middleware/auth.ts +98 -9
- package/src/server/middleware/index.ts +2 -1
- package/src/server/middleware/settings.ts +26 -0
- package/src/server/services/access.test.ts +197 -0
- package/src/server/services/access.ts +254 -6
- package/src/server/services/index.ts +14 -11
- package/src/server/services/pagination.ts +22 -0
- package/src/server/time.ts +45 -0
- package/src/services/account-lifecycle/index.ts +142 -18
- package/src/services/accounts/app.ts +658 -170
- package/src/services/accounts/authz.test.ts +77 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/entities.ts +84 -5
- package/src/services/accounts/groups.ts +30 -24
- package/src/services/accounts/model.test.ts +30 -0
- package/src/services/accounts/switching.test.ts +14 -0
- package/src/services/accounts/switching.ts +15 -6
- package/src/services/accounts/users.ts +75 -52
- package/src/services/announcements/index.test.ts +32 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/audit/index.test.ts +84 -0
- package/src/services/audit/index.ts +431 -0
- package/src/services/auth-flows/index.ts +9 -2
- package/src/services/auth-flows/ipa.ts +0 -2
- package/src/services/auth-flows/magic-link.ts +3 -2
- package/src/services/auth-flows/password-reset.ts +284 -0
- package/src/services/auth-flows/proxy-return.test.ts +24 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/gateway.ts +162 -0
- package/src/services/index.ts +44 -2
- package/src/services/ipa/effective-groups.test.ts +33 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/profile.ts +45 -3
- package/src/services/ipa/search.ts +3 -5
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.test.ts +32 -0
- package/src/services/ipa/sync-planning.ts +22 -0
- package/src/services/ipa/sync.ts +110 -38
- package/src/services/oauth-tokens.ts +104 -0
- package/src/services/postgres.ts +21 -6
- package/src/services/providers/local/auth.test.ts +22 -0
- package/src/services/providers/local/auth.ts +46 -3
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.test.ts +210 -0
- package/src/services/service-account-credentials.ts +715 -0
- package/src/services/service-accounts.ts +188 -0
- package/src/services/session/index.ts +7 -8
- package/src/services/settings/app.ts +4 -20
- package/src/services/settings/defaults.ts +64 -22
- package/src/services/settings/store.ts +47 -0
- package/src/services/weather/forecast.ts +40 -7
- package/src/services/webauthn.test.ts +36 -0
- package/src/services/webauthn.ts +384 -0
- package/src/shared/icons.ts +391 -100
- package/src/shared/index.ts +7 -0
- package/src/shared/markdown/extensions/code.ts +38 -1
- package/src/shared/markdown/extensions/images.ts +39 -3
- package/src/shared/markdown/extensions/info-blocks.ts +5 -5
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +79 -58
- package/src/shared/markdown/formula.test.ts +1089 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +76 -2
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/redirect.test.ts +49 -0
- package/src/shared/redirect.ts +52 -0
- package/src/shared/theme.test.ts +24 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +7 -3
- package/src/ssr/AdminSidebar.tsx +115 -49
- package/src/ssr/AppLaunchpad.island.tsx +176 -0
- package/src/ssr/Footer.island.tsx +3 -8
- package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
- package/src/ssr/GlobalSearchDialog.tsx +545 -117
- package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
- package/src/ssr/Layout.tsx +74 -66
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutHelp.tsx +266 -0
- package/src/ssr/NavMenu.island.tsx +0 -39
- package/src/ssr/ThemeToggleRail.island.tsx +3 -3
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/islands/index.ts +13 -0
- package/src/styles/base-popover.css +5 -2
- package/src/styles/effects.css +87 -6
- package/src/styles/global.css +146 -9
- package/src/styles/input.css +3 -1
- package/src/styles/utilities-buttons.css +133 -27
- package/src/styles/utilities-code-display.css +67 -0
- package/src/styles/utilities-completion.css +223 -0
- package/src/styles/utilities-detail.css +73 -0
- package/src/styles/utilities-feedback.css +16 -15
- package/src/styles/utilities-layout.css +42 -2
- package/src/styles/utilities-markdown-editor.css +472 -0
- package/src/styles/utilities-navigation.css +63 -8
- package/src/styles/utilities-script.css +84 -0
- package/src/styles/utilities-table-tile.css +229 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/ui/completion/behaviors.test.ts +95 -0
- package/src/ui/completion/behaviors.ts +205 -0
- package/src/ui/completion/engine.ts +368 -0
- package/src/ui/completion/index.ts +40 -0
- package/src/ui/completion/overlay.ts +92 -0
- package/src/ui/dialog-core.ts +173 -45
- package/src/ui/filter/FilterChip.tsx +42 -40
- package/src/ui/index.ts +11 -12
- package/src/ui/input/AutocompleteEditor.tsx +656 -0
- package/src/ui/input/CheckboxCard.tsx +91 -0
- package/src/ui/input/Combobox.tsx +375 -0
- package/src/ui/input/DatePicker.tsx +846 -0
- package/src/ui/input/DateTimeInput.tsx +29 -4
- package/src/ui/input/FileDropzone.tsx +116 -0
- package/src/ui/input/IconInput.tsx +116 -0
- package/src/ui/input/ImageInput.tsx +19 -2
- package/src/ui/input/MultiSelectInput.tsx +448 -0
- package/src/ui/input/NumberInput.tsx +417 -61
- package/src/ui/input/SegmentedControl.tsx +2 -2
- package/src/ui/input/Select.tsx +172 -10
- package/src/ui/input/Slider.tsx +3 -4
- package/src/ui/input/Switch.tsx +3 -2
- package/src/ui/input/TemplateEditor.tsx +212 -0
- package/src/ui/input/TextInput.tsx +144 -13
- package/src/ui/input/index.ts +53 -8
- package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
- package/src/ui/input/markdown/Toolbar.tsx +90 -0
- package/src/ui/input/markdown/actions.ts +233 -0
- package/src/ui/input/markdown/active-formats.ts +94 -0
- package/src/ui/input/markdown/behaviors.ts +193 -0
- package/src/ui/input/markdown/code-zone.ts +23 -0
- package/src/ui/input/markdown/highlight.ts +316 -0
- package/src/ui/layout.ts +22 -0
- package/src/ui/misc/AppOverview.tsx +105 -0
- package/src/ui/misc/AppWorkspace.tsx +607 -0
- package/src/ui/misc/Calendar.tsx +1291 -0
- package/src/ui/misc/Chart.tsx +162 -0
- package/src/ui/misc/CodeDisplay.tsx +54 -0
- package/src/ui/misc/ContextMenu.tsx +2 -2
- package/src/ui/misc/DataTable.tsx +269 -0
- package/src/ui/misc/DockWorkspace.tsx +425 -0
- package/src/ui/misc/Docs.tsx +153 -0
- package/src/ui/misc/Dropdown.tsx +2 -2
- package/src/ui/misc/EntitySearch.tsx +260 -129
- package/src/ui/misc/LinkCard.tsx +14 -2
- package/src/ui/misc/LogEntriesTable.tsx +34 -31
- package/src/ui/misc/Pagination.tsx +31 -12
- package/src/ui/misc/PanelDialog.tsx +109 -0
- package/src/ui/misc/Panes.tsx +873 -0
- package/src/ui/misc/PermissionEditor.tsx +358 -262
- package/src/ui/misc/Placeholder.tsx +40 -0
- package/src/ui/misc/ProgressBar.tsx +1 -1
- package/src/ui/misc/ResourceApiKeys.tsx +260 -0
- package/src/ui/misc/SettingsModal.tsx +150 -0
- package/src/ui/misc/StatCell.tsx +182 -40
- package/src/ui/misc/StatGrid.tsx +149 -0
- package/src/ui/misc/StructuredDataPreview.tsx +107 -0
- package/src/ui/misc/code-highlight.ts +213 -0
- package/src/ui/misc/index.ts +93 -12
- package/src/ui/prompts.tsx +362 -312
- package/src/ui/toast.ts +384 -0
- package/src/ui/widgets/Widget.tsx +12 -4
- package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
- package/src/ui/ipa/GroupView.tsx +0 -36
- package/src/ui/ipa/LoginBtn.tsx +0 -16
- package/src/ui/ipa/UserView.tsx +0 -58
- package/src/ui/ipa/index.ts +0 -4
- package/src/ui/navigation.ts +0 -32
- package/src/ui/sidebar.tsx +0 -468
- /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
|
@@ -9,13 +9,26 @@
|
|
|
9
9
|
import type { MarkedExtension, Tokens } from "marked";
|
|
10
10
|
import { escapeHtml } from "../shared";
|
|
11
11
|
|
|
12
|
+
/** Base64-encode a UTF-8 string for embedding in a `data-` attribute.
|
|
13
|
+
* Server-safe: works in Bun runtime. The matching client-side decoder
|
|
14
|
+
* uses `atob` + `decodeURIComponent` (see `frontend/lib/script/read-mode.ts`). */
|
|
15
|
+
const encodeScriptSource = (source: string): string => {
|
|
16
|
+
// `Buffer` exists in Bun's server-side runtime. The fallback path
|
|
17
|
+
// (`unescape(encodeURIComponent(...))` + `btoa`) is for any
|
|
18
|
+
// browser/edge environment that imports this module without Buffer.
|
|
19
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source, "utf8").toString("base64");
|
|
20
|
+
return btoa(unescape(encodeURIComponent(source)));
|
|
21
|
+
};
|
|
22
|
+
|
|
12
23
|
export function codeExtension(): MarkedExtension {
|
|
13
24
|
return {
|
|
14
25
|
renderer: {
|
|
15
26
|
code(token: Tokens.Code): string {
|
|
16
27
|
const { text, lang } = token;
|
|
17
28
|
const escapedCode = escapeHtml(text);
|
|
18
|
-
const
|
|
29
|
+
const langLower = lang?.toLowerCase();
|
|
30
|
+
const isMermaid = langLower === "mermaid";
|
|
31
|
+
const isScript = langLower === "script";
|
|
19
32
|
|
|
20
33
|
// Language class for syntax highlighting / mermaid detection
|
|
21
34
|
const langClass = lang ? ` language-${escapeHtml(lang)}` : "";
|
|
@@ -34,6 +47,30 @@ export function codeExtension(): MarkedExtension {
|
|
|
34
47
|
);
|
|
35
48
|
}
|
|
36
49
|
|
|
50
|
+
// ```script blocks: emit a wrapper carrying the source as a
|
|
51
|
+
// base64 `data-` attribute + an empty output container. The
|
|
52
|
+
// client-side `enhanceReadModeScripts` (see frontend/lib/script
|
|
53
|
+
// /read-mode.ts) finds these wrappers, decodes the source, and
|
|
54
|
+
// either runs it (when notebook.scriptsEnabled is true) or
|
|
55
|
+
// shows the source as a regular code block (when false).
|
|
56
|
+
// Decision is made client-side because the markdown layer is
|
|
57
|
+
// notebook-agnostic — `scriptsEnabled` is a per-notebook flag.
|
|
58
|
+
// The fallback (source) stays in the DOM (just `display: none`
|
|
59
|
+
// when scripts are active) so view-source / accessibility
|
|
60
|
+
// tooling sees the original code. Skip the carrier when
|
|
61
|
+
// there's no source — empty fences shouldn't activate.
|
|
62
|
+
if (isScript) {
|
|
63
|
+
const sourceB64 = encodeScriptSource(text);
|
|
64
|
+
return (
|
|
65
|
+
`<div class="md-script-block my-3" data-script-source="${sourceB64}">` +
|
|
66
|
+
`<pre class="md-script-source bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-md p-4 overflow-x-auto">` +
|
|
67
|
+
`<code class="text-sm font-mono text-gray-800 dark:text-gray-200 whitespace-pre language-script">${escapedCode}</code>` +
|
|
68
|
+
`</pre>` +
|
|
69
|
+
`<div class="md-script-output"></div>` +
|
|
70
|
+
`</div>`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
37
74
|
// Language badge if specified
|
|
38
75
|
const langBadge = lang
|
|
39
76
|
? `<span class="absolute top-2 right-2 text-xs text-gray-400 dark:text-gray-500 font-mono select-none">${escapeHtml(lang)}</span>`
|
|
@@ -5,22 +5,58 @@
|
|
|
5
5
|
* - Centered figure with max-height constraint
|
|
6
6
|
* - Rounded border
|
|
7
7
|
* - Optional caption from alt text
|
|
8
|
+
*
|
|
9
|
+
* Supports an optional Pandoc/HFM-style size suffix on the URL:
|
|
10
|
+
*
|
|
11
|
+
*  // both
|
|
12
|
+
*  // width only
|
|
13
|
+
*  // height only
|
|
14
|
+
*
|
|
15
|
+
* Marked's default tokenizer treats the trailing ` =WxH` as part of the
|
|
16
|
+
* URL — we parse and strip it here before emitting the `<img>` tag.
|
|
8
17
|
*/
|
|
9
18
|
|
|
10
19
|
import type { MarkedExtension, Tokens } from "marked";
|
|
11
20
|
import { escapeHtml, IMAGE_STYLES } from "../shared";
|
|
12
21
|
|
|
22
|
+
const SIZE_SUFFIX_REGEX = /\s+=(\d+)?x(\d+)?$/;
|
|
23
|
+
|
|
24
|
+
type ParsedImage = {
|
|
25
|
+
href: string;
|
|
26
|
+
width: string | null;
|
|
27
|
+
height: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const parseImageUrl = (href: string): ParsedImage => {
|
|
31
|
+
const match = SIZE_SUFFIX_REGEX.exec(href);
|
|
32
|
+
if (!match) return { href, width: null, height: null };
|
|
33
|
+
return {
|
|
34
|
+
href: href.slice(0, match.index),
|
|
35
|
+
width: match[1] ?? null,
|
|
36
|
+
height: match[2] ?? null,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
13
40
|
export function imagesExtension(): MarkedExtension {
|
|
14
41
|
return {
|
|
15
42
|
renderer: {
|
|
16
43
|
image(token: Tokens.Image): string {
|
|
17
|
-
const { href, title, text: alt } = token;
|
|
44
|
+
const { href: rawHref, title, text: alt } = token;
|
|
45
|
+
const { href, width, height } = parseImageUrl(rawHref);
|
|
18
46
|
|
|
19
47
|
// Build the image element
|
|
20
48
|
const imgAttrs = [`src="${escapeHtml(href)}"`, `alt="${escapeHtml(alt || "")}"`, `loading="lazy"`, `class="${IMAGE_STYLES.img}"`];
|
|
21
49
|
|
|
22
|
-
if (title) {
|
|
23
|
-
|
|
50
|
+
if (title) imgAttrs.push(`title="${escapeHtml(title)}"`);
|
|
51
|
+
if (width) imgAttrs.push(`width="${width}"`);
|
|
52
|
+
if (height) imgAttrs.push(`height="${height}"`);
|
|
53
|
+
|
|
54
|
+
// Inline style overrides the global `max-height: 400px` from
|
|
55
|
+
// IMAGE_STYLES.img only when the user opted into a custom size.
|
|
56
|
+
if (width || height) {
|
|
57
|
+
const styles: string[] = ["max-height: none"];
|
|
58
|
+
if (width) styles.push(`max-width: ${width}px`);
|
|
59
|
+
imgAttrs.push(`style="${styles.join("; ")}"`);
|
|
24
60
|
}
|
|
25
61
|
|
|
26
62
|
const imgHtml = `<img ${imgAttrs.join(" ")} />`;
|
|
@@ -18,27 +18,27 @@ const blockConfig: Record<BlockType, { icon: string; label: string; classes: str
|
|
|
18
18
|
note: {
|
|
19
19
|
icon: "ti-chevron-right",
|
|
20
20
|
label: "Note",
|
|
21
|
-
classes: "
|
|
21
|
+
classes: "info-block-note",
|
|
22
22
|
},
|
|
23
23
|
info: {
|
|
24
24
|
icon: "ti-info-circle",
|
|
25
25
|
label: "Info",
|
|
26
|
-
classes: "
|
|
26
|
+
classes: "info-block-info",
|
|
27
27
|
},
|
|
28
28
|
success: {
|
|
29
29
|
icon: "ti-check",
|
|
30
30
|
label: "Success",
|
|
31
|
-
classes: "
|
|
31
|
+
classes: "info-block-success",
|
|
32
32
|
},
|
|
33
33
|
warning: {
|
|
34
34
|
icon: "ti-alert-circle",
|
|
35
35
|
label: "Warning",
|
|
36
|
-
classes: "
|
|
36
|
+
classes: "info-block-warning",
|
|
37
37
|
},
|
|
38
38
|
danger: {
|
|
39
39
|
icon: "ti-alert-hexagon",
|
|
40
40
|
label: "Danger",
|
|
41
|
-
classes: "
|
|
41
|
+
classes: "info-block-danger",
|
|
42
42
|
},
|
|
43
43
|
};
|
|
44
44
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark / Highlight extension for marked.
|
|
3
|
+
*
|
|
4
|
+
* Pandoc + HFM-style highlighting:
|
|
5
|
+
*
|
|
6
|
+
* ==marked text==
|
|
7
|
+
*
|
|
8
|
+
* Renders to a `<mark>` element with a yellow textmarker-style background.
|
|
9
|
+
* Inner content is parsed as inline markdown so users can mix bold / italic
|
|
10
|
+
* inside highlights.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
14
|
+
|
|
15
|
+
const MARK_CLASSES = "bg-yellow-300 dark:bg-yellow-400/40 text-zinc-900 dark:text-yellow-50 px-0.5 rounded";
|
|
16
|
+
|
|
17
|
+
export function markExtension(): MarkedExtension {
|
|
18
|
+
return {
|
|
19
|
+
extensions: [
|
|
20
|
+
{
|
|
21
|
+
name: "mark",
|
|
22
|
+
level: "inline",
|
|
23
|
+
start(src: string) {
|
|
24
|
+
// Hint marked at where to start scanning. Two consecutive `=` is
|
|
25
|
+
// cheap to find and unique enough.
|
|
26
|
+
return src.match(/==(?!=)/)?.index;
|
|
27
|
+
},
|
|
28
|
+
tokenizer(src: string) {
|
|
29
|
+
// The inner content must:
|
|
30
|
+
// - not start or end with whitespace (matches Pandoc behaviour)
|
|
31
|
+
// - not contain `=` itself (avoids overlapping with `===` etc.)
|
|
32
|
+
const match = /^==(?!=)([^\s=][^=]*?[^\s=]|[^\s=])==(?!=)/.exec(src);
|
|
33
|
+
if (!match) return undefined;
|
|
34
|
+
return {
|
|
35
|
+
type: "mark",
|
|
36
|
+
raw: match[0],
|
|
37
|
+
text: match[1] ?? "",
|
|
38
|
+
tokens: this.lexer.inlineTokens(match[1] ?? ""),
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
renderer(token: Tokens.Generic) {
|
|
42
|
+
const inner = this.parser.parseInline(token.tokens ?? []);
|
|
43
|
+
return `<mark class="${MARK_CLASSES}">${inner}</mark>`;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscript and Superscript extensions for marked.
|
|
3
|
+
*
|
|
4
|
+
* H~2~O → H<sub>2</sub>O
|
|
5
|
+
* E=mc^2^ → E=mc<sup>2</sup>
|
|
6
|
+
*
|
|
7
|
+
* Conservative tokenisers: the inner content cannot contain whitespace or
|
|
8
|
+
* the marker character. That avoids accidentally swallowing strikethrough
|
|
9
|
+
* (`~~text~~`), regular tildes used as separators, or `^` used as the
|
|
10
|
+
* regex caret in inline code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
14
|
+
import { escapeHtml } from "../shared";
|
|
15
|
+
|
|
16
|
+
export function subSupExtension(): MarkedExtension {
|
|
17
|
+
return {
|
|
18
|
+
extensions: [
|
|
19
|
+
{
|
|
20
|
+
name: "subscript",
|
|
21
|
+
level: "inline",
|
|
22
|
+
start(src: string) {
|
|
23
|
+
// Single `~` not preceded or followed by another `~`.
|
|
24
|
+
return src.match(/(?<!~)~(?!~)/)?.index;
|
|
25
|
+
},
|
|
26
|
+
tokenizer(src: string) {
|
|
27
|
+
const match = /^(?<!~)~(?!~)([^~\s]+)~(?!~)/.exec(src);
|
|
28
|
+
if (!match) return undefined;
|
|
29
|
+
return {
|
|
30
|
+
type: "subscript",
|
|
31
|
+
raw: match[0],
|
|
32
|
+
text: match[1] ?? "",
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
renderer(token: Tokens.Generic) {
|
|
36
|
+
return `<sub>${escapeHtml(String(token.text))}</sub>`;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "superscript",
|
|
41
|
+
level: "inline",
|
|
42
|
+
start(src: string) {
|
|
43
|
+
return src.match(/\^/)?.index;
|
|
44
|
+
},
|
|
45
|
+
tokenizer(src: string) {
|
|
46
|
+
const match = /^\^([^\^\s]+)\^/.exec(src);
|
|
47
|
+
if (!match) return undefined;
|
|
48
|
+
return {
|
|
49
|
+
type: "superscript",
|
|
50
|
+
raw: match[0],
|
|
51
|
+
text: match[1] ?? "",
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
renderer(token: Tokens.Generic) {
|
|
55
|
+
return `<sup>${escapeHtml(String(token.text))}</sup>`;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,87 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Tile-style markdown table renderer for `marked`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Emits the three-layer markup that `utilities-table-tile.css` styles:
|
|
5
|
+
* <div class="md-table-wrap"> — horizontal scroll
|
|
6
|
+
* <table class="md-table"> — layout reset
|
|
7
|
+
* <th|td>
|
|
8
|
+
* <span class="md-table-cell"> — visible tile
|
|
9
|
+
*
|
|
10
|
+
* Per-column alignment from `:---:` syntax flows through `token.align`
|
|
11
|
+
* to a `md-align-{left,center,right}` class on each cell.
|
|
12
|
+
*
|
|
13
|
+
* Cells starting with `=` are evaluated as formulas via the shared
|
|
14
|
+
* `formula.ts` module. The computed value replaces the cell text;
|
|
15
|
+
* errors render as a red-text cell with a hover-title showing the
|
|
16
|
+
* full diagnostic. The same evaluator runs in the editor widget so
|
|
17
|
+
* read-mode HTML and rich-edit preview stay in sync.
|
|
18
|
+
*
|
|
19
|
+
* Markdown tables are hand-edited and small — no pagination, no
|
|
20
|
+
* NULL/datetime cell-formatting, no zebra columns. If you need any of
|
|
21
|
+
* those, you're rendering data not prose; use the Grids app.
|
|
6
22
|
*/
|
|
7
|
-
|
|
8
23
|
import type { MarkedExtension, Tokens } from "marked";
|
|
24
|
+
import { evaluateFormula, formatValue, isFormula, isTotalRow, parseProgressValue, type EvalContext, type ProgressValue } from "../formula";
|
|
9
25
|
import { escapeHtml } from "../shared";
|
|
10
26
|
|
|
11
|
-
|
|
12
|
-
const trimmed = cell.trim();
|
|
27
|
+
type Align = "left" | "right" | "center" | null;
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
const alignClass = (align: Align): string => {
|
|
30
|
+
if (align === "right") return " md-align-right";
|
|
31
|
+
if (align === "center") return " md-align-center";
|
|
32
|
+
return "";
|
|
33
|
+
};
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
/** Best-effort plain-text extraction for formula evaluation. The
|
|
36
|
+
* formula evaluator only needs the raw cell value (numbers, strings),
|
|
37
|
+
* so we walk the marked-token tree and collect the text — falling
|
|
38
|
+
* back to `cell.text` if anything goes wrong. */
|
|
39
|
+
const cellText = (cell: Tokens.TableCell): string => {
|
|
40
|
+
if (typeof cell.text === "string" && cell.text.length > 0) return cell.text;
|
|
41
|
+
return "";
|
|
42
|
+
};
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return date.toLocaleString();
|
|
32
|
-
} catch {
|
|
33
|
-
return escapeHtml(trimmed);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
44
|
+
const renderProgressCell = (progress: ProgressValue, alignCls: string, title: string): string => {
|
|
45
|
+
const pct = Math.round(progress.ratio * 100);
|
|
46
|
+
return `<td><span class="md-table-cell md-table-progress${alignCls}" title="${escapeHtml(title)}"><span class="md-table-progress-track" aria-hidden="true"><span class="md-table-progress-fill" style="width:${pct}%"></span></span><span>${escapeHtml(progress.label)}</span></span></td>`;
|
|
47
|
+
};
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return num.toFixed(4).replace(/\.?0+$/, "");
|
|
49
|
+
const renderCell = (alignCls: string, originalText: string, ctx: EvalContext, htmlContent: string): string => {
|
|
50
|
+
if (!isFormula(originalText)) {
|
|
51
|
+
return `<td><span class="md-table-cell${alignCls}">${htmlContent}</span></td>`;
|
|
41
52
|
}
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
const result = evaluateFormula(originalText, ctx);
|
|
54
|
+
if (result.kind === "ok") {
|
|
55
|
+
const progress = parseProgressValue(result.value);
|
|
56
|
+
if (progress) return renderProgressCell(progress, alignCls, originalText);
|
|
57
|
+
// Computed cells get a `ti-math-function` icon prefix + blue text
|
|
58
|
+
// so the user can tell at a glance which values are derived vs
|
|
59
|
+
// hand-typed. Hover-title shows the original formula source.
|
|
60
|
+
return `<td><span class="md-table-cell md-formula-ok${alignCls}" title="${escapeHtml(originalText)}"><i class="ti ti-math-function"></i>${escapeHtml(formatValue(result.value))}</span></td>`;
|
|
61
|
+
}
|
|
62
|
+
// Error: show the original formula in red + ⚠ icon, hover title
|
|
63
|
+
// carries the full diagnostic. Pattern matches `md-formula-error`
|
|
64
|
+
// styling in `utilities-table-tile.css`.
|
|
65
|
+
const tooltip = result.suggestion ? `${result.message}\n→ Suggestion: ${result.suggestion}` : result.message;
|
|
66
|
+
return `<td><span class="md-table-cell md-formula-error${alignCls}" title="${escapeHtml(tooltip)}">⚠ ${escapeHtml(originalText)}</span></td>`;
|
|
44
67
|
};
|
|
45
68
|
|
|
46
69
|
export function tablesExtension(): MarkedExtension {
|
|
47
70
|
return {
|
|
48
71
|
renderer: {
|
|
49
72
|
table(token: Tokens.Table): string {
|
|
73
|
+
const align = token.align ?? [];
|
|
74
|
+
|
|
75
|
+
// Build the EvalContext once per table — formulas reference
|
|
76
|
+
// raw cell text, not other formulas' results, so a single pass
|
|
77
|
+
// through the source rows is enough.
|
|
78
|
+
const headers = token.header.map((h) => cellText(h));
|
|
79
|
+
const rawRows = token.rows.map((row) => row.map((c) => cellText(c)));
|
|
80
|
+
|
|
50
81
|
const headerCells = token.header
|
|
51
|
-
.map(
|
|
52
|
-
(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)}</th>`,
|
|
56
|
-
)
|
|
82
|
+
.map((cell, i) => {
|
|
83
|
+
const cls = `md-table-cell${alignClass(align[i] ?? null)}`;
|
|
84
|
+
return `<th><span class="${cls}">${this.parser.parseInline(cell.tokens)}</span></th>`;
|
|
85
|
+
})
|
|
57
86
|
.join("");
|
|
58
87
|
|
|
59
88
|
const rows = token.rows
|
|
60
|
-
.map((row) => {
|
|
89
|
+
.map((row, rowIdx) => {
|
|
90
|
+
const rowTexts = rawRows[rowIdx] ?? [];
|
|
91
|
+
const totalRow = isTotalRow(rowTexts);
|
|
61
92
|
const cells = row
|
|
62
|
-
.map((cell,
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
93
|
+
.map((cell, colIdx) => {
|
|
94
|
+
const alignCls = alignClass(align[colIdx] ?? null);
|
|
95
|
+
const original = cellText(cell);
|
|
96
|
+
const htmlContent = this.parser.parseInline(cell.tokens);
|
|
97
|
+
const ctx: EvalContext = { headers, rows: rawRows, currentRow: rowIdx, currentCol: colIdx };
|
|
98
|
+
return renderCell(alignCls, original, ctx, htmlContent);
|
|
66
99
|
})
|
|
67
100
|
.join("");
|
|
68
|
-
return `<tr class="
|
|
101
|
+
return totalRow ? `<tr class="md-table-total-row">${cells}</tr>` : `<tr>${cells}</tr>`;
|
|
69
102
|
})
|
|
70
|
-
.join("
|
|
71
|
-
|
|
72
|
-
const rowCount = token.rows.length;
|
|
103
|
+
.join("");
|
|
73
104
|
|
|
74
|
-
return `<div class="
|
|
75
|
-
<div class="flex flex-col">
|
|
76
|
-
<div class="overflow-x-auto rounded">
|
|
77
|
-
<table class="min-w-full text-sm tabular-nums">
|
|
78
|
-
<thead><tr>${headerCells}</tr></thead>
|
|
79
|
-
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">${rows}</tbody>
|
|
80
|
-
</table>
|
|
81
|
-
</div>
|
|
82
|
-
<div class="text-center text-xs mt-2">${rowCount} row${rowCount === 1 ? "" : "s"}</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>`;
|
|
105
|
+
return `<div class="md-table-wrap"><table class="md-table"><thead><tr>${headerCells}</tr></thead><tbody>${rows}</tbody></table></div>`;
|
|
85
106
|
},
|
|
86
107
|
},
|
|
87
108
|
};
|