@tangle-network/ui 3.0.0 → 4.1.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/chat.js +8 -7
  3. package/dist/{chunk-TMFOPHHN.js → chunk-52Y3FMFI.js} +2 -2
  4. package/dist/{chunk-7UO2ZMRQ.js → chunk-5VPTNXX7.js} +2 -2
  5. package/dist/{chunk-XIHMJ7ZQ.js → chunk-AAUNOHVL.js} +5 -30
  6. package/dist/{chunk-YJ2G3XO5.js → chunk-CMX2I43A.js} +1 -1
  7. package/dist/{chunk-2VH6PUXD.js → chunk-DGW77LD7.js} +1 -1
  8. package/dist/{chunk-CD53GZOM.js → chunk-FJBTCTZM.js} +1 -1
  9. package/dist/{chunk-YNN4O57I.js → chunk-JBPWIYTQ.js} +4 -4
  10. package/dist/{chunk-2NFQRQOD.js → chunk-KT5RNO7N.js} +4 -4
  11. package/dist/{chunk-HJKCSXCH.js → chunk-LELGOLFV.js} +44 -78
  12. package/dist/{chunk-EEE55AVS.js → chunk-SZ44QDA6.js} +1 -1
  13. package/dist/{chunk-66BNMOVT.js → chunk-WUQDUBJG.js} +5 -4
  14. package/dist/chunk-ZRVH3WCA.js +107 -0
  15. package/dist/{code-block-DjXf8eOG.d.ts → code-block-0kSpWMnf.d.ts} +7 -1
  16. package/dist/{document-editor-pane-A5LT5H4N.js → document-editor-pane-WCTA3ZOE.js} +3 -3
  17. package/dist/editor.js +3 -3
  18. package/dist/files.d.ts +39 -2
  19. package/dist/files.js +16 -4
  20. package/dist/hooks.js +5 -5
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +22 -10
  23. package/dist/markdown.d.ts +1 -1
  24. package/dist/markdown.js +2 -2
  25. package/dist/openui.js +3 -3
  26. package/dist/primitives.d.ts +1 -1
  27. package/dist/primitives.js +1 -1
  28. package/dist/run.js +8 -7
  29. package/dist/sdk-hooks.js +5 -5
  30. package/dist/tool-previews.js +3 -2
  31. package/package.json +2 -2
  32. package/src/files/file-artifact-pane.tsx +3 -3
  33. package/src/files/file-format.test.ts +176 -0
  34. package/src/files/file-format.ts +167 -0
  35. package/src/files/file-preview.stories.tsx +87 -0
  36. package/src/files/file-preview.test.tsx +52 -0
  37. package/src/files/file-preview.tsx +48 -94
  38. package/src/files/index.ts +8 -0
  39. package/src/markdown/code-block.test.tsx +62 -0
  40. package/src/markdown/code-block.tsx +11 -4
  41. package/src/tool-previews/write-file-preview.tsx +2 -30
package/dist/files.js CHANGED
@@ -5,10 +5,17 @@ import {
5
5
  FileTree,
6
6
  RichFileTree,
7
7
  filterFileTree
8
- } from "./chunk-HJKCSXCH.js";
8
+ } from "./chunk-LELGOLFV.js";
9
+ import {
10
+ detectFileFormat,
11
+ fileExtension,
12
+ getCodeLanguage,
13
+ getFormatLabel,
14
+ getSyntaxLanguage
15
+ } from "./chunk-ZRVH3WCA.js";
9
16
  import "./chunk-CSAIKY36.js";
10
- import "./chunk-CD53GZOM.js";
11
- import "./chunk-66BNMOVT.js";
17
+ import "./chunk-FJBTCTZM.js";
18
+ import "./chunk-WUQDUBJG.js";
12
19
  import "./chunk-RQHJBTEU.js";
13
20
  export {
14
21
  FileArtifactPane,
@@ -16,5 +23,10 @@ export {
16
23
  FileTabs,
17
24
  FileTree,
18
25
  RichFileTree,
19
- filterFileTree
26
+ detectFileFormat,
27
+ fileExtension,
28
+ filterFileTree,
29
+ getCodeLanguage,
30
+ getFormatLabel,
31
+ getSyntaxLanguage
20
32
  };
package/dist/hooks.js CHANGED
@@ -11,18 +11,18 @@ import {
11
11
  useSSEStream,
12
12
  useSdkSession,
13
13
  useToolCallStream
14
- } from "./chunk-YJ2G3XO5.js";
14
+ } from "./chunk-CMX2I43A.js";
15
15
  import "./chunk-OEX7NZE3.js";
16
16
  import {
17
17
  useAutoScroll,
18
18
  useRunCollapseState,
19
19
  useRunGroups
20
20
  } from "./chunk-54SQQMMM.js";
21
- import "./chunk-7UO2ZMRQ.js";
22
- import "./chunk-2VH6PUXD.js";
21
+ import "./chunk-5VPTNXX7.js";
22
+ import "./chunk-DGW77LD7.js";
23
23
  import "./chunk-BX6AQMUS.js";
24
- import "./chunk-CD53GZOM.js";
25
- import "./chunk-66BNMOVT.js";
24
+ import "./chunk-FJBTCTZM.js";
25
+ import "./chunk-WUQDUBJG.js";
26
26
  import "./chunk-RQHJBTEU.js";
27
27
  export {
28
28
  RealtimeSessionRegistry,
package/dist/index.d.ts CHANGED
@@ -6,9 +6,9 @@ export { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, Agen
6
6
  export { ExpandedToolDetail, ExpandedToolDetailProps, InlineThinkingItem, InlineThinkingItemProps, InlineToolItem, InlineToolItemProps, LiveDuration, RunGroup, RunGroupProps } from './run.js';
7
7
  export { F as FeedSegment, T as ToolCallData, a as ToolCallFeed, b as ToolCallFeedProps, c as ToolCallGroup, d as ToolCallGroupProps, e as ToolCallStatus, f as ToolCallStep, g as ToolCallStepProps, h as ToolCallType, p as parseToolEvent } from './tool-call-feed-Bs3MyQMT.js';
8
8
  export { OpenUIAction, OpenUIActionsNode, OpenUIArtifactRenderer, OpenUIArtifactRendererProps, OpenUIBadgeNode, OpenUICardNode, OpenUICodeNode, OpenUIComponentNode, OpenUIGridNode, OpenUIHeadingNode, OpenUIKeyValueNode, OpenUIMarkdownNode, OpenUIPrimitive, OpenUISeparatorNode, OpenUIStackNode, OpenUIStatNode, OpenUITableNode, OpenUITextNode } from './openui.js';
9
- export { FileArtifactPane, FileArtifactPaneProps, FileNode, FilePreview, FilePreviewProps, FileTabData, FileTabs, FileTabsProps, FileTree, FileTreeProps, FileTreeVisibilityOptions, RichFileTree, RichFileTreeGitEntry, RichFileTreeGitStatus, RichFileTreeProps, RichFileTreeThemeVars, filterFileTree } from './files.js';
9
+ export { FileArtifactPane, FileArtifactPaneProps, FileFormat, FileNode, FilePreview, FilePreviewProps, FileTabData, FileTabs, FileTabsProps, FileTree, FileTreeProps, FileTreeVisibilityOptions, RichFileTree, RichFileTreeGitEntry, RichFileTreeGitStatus, RichFileTreeProps, RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage } from './files.js';
10
10
  export { Markdown, MarkdownProps } from './markdown.js';
11
- export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-DjXf8eOG.js';
11
+ export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-0kSpWMnf.js';
12
12
  export { AuthHeader, AuthHeaderProps, GitHubLoginButton, GitHubLoginButtonProps, LoginLayout, LoginLayoutProps, SessionUser, UserMenu, UserMenuProps } from './auth.js';
13
13
  export { AgentStreamEvent, AppendUserMessageOptions, ApplySdkEventOptions, AutomationStreamEvent, BeginAssistantMessageOptions, BotStreamEvent, CompleteAssistantMessageOptions, ConnectionState, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, TaskStreamEvent, TerminalStreamEvent, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, useAutoScroll, useDropdownMenu, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useToolCallStream } from './sdk-hooks.js';
14
14
  export { AuthUser, UseAuthOptions, UseAuthResult, createAuthFetcher, useApiKey, useAuth, useLiveTime } from './hooks.js';
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  useSSEStream,
18
18
  useSdkSession,
19
19
  useToolCallStream
20
- } from "./chunk-YJ2G3XO5.js";
20
+ } from "./chunk-CMX2I43A.js";
21
21
  import {
22
22
  addMessage,
23
23
  addParts,
@@ -138,7 +138,7 @@ import {
138
138
  MessageList,
139
139
  ThinkingIndicator,
140
140
  UserMessage
141
- } from "./chunk-2NFQRQOD.js";
141
+ } from "./chunk-KT5RNO7N.js";
142
142
  import {
143
143
  useAutoScroll,
144
144
  useRunCollapseState,
@@ -148,18 +148,18 @@ import "./chunk-LQS34IGP.js";
148
148
  import {
149
149
  ToolCallFeed,
150
150
  parseToolEvent
151
- } from "./chunk-7UO2ZMRQ.js";
151
+ } from "./chunk-5VPTNXX7.js";
152
152
  import {
153
153
  ExpandedToolDetail,
154
154
  InlineThinkingItem,
155
155
  InlineToolItem,
156
156
  LiveDuration,
157
157
  RunGroup
158
- } from "./chunk-YNN4O57I.js";
158
+ } from "./chunk-JBPWIYTQ.js";
159
159
  import {
160
160
  ToolCallGroup,
161
161
  ToolCallStep
162
- } from "./chunk-2VH6PUXD.js";
162
+ } from "./chunk-DGW77LD7.js";
163
163
  import {
164
164
  formatBytes,
165
165
  formatDuration,
@@ -180,11 +180,11 @@ import {
180
180
  QuestionPreview,
181
181
  WebSearchPreview,
182
182
  WriteFilePreview
183
- } from "./chunk-XIHMJ7ZQ.js";
183
+ } from "./chunk-AAUNOHVL.js";
184
184
  import "./chunk-RQGKSCEZ.js";
185
185
  import {
186
186
  OpenUIArtifactRenderer
187
- } from "./chunk-TMFOPHHN.js";
187
+ } from "./chunk-52Y3FMFI.js";
188
188
  import {
189
189
  Badge,
190
190
  Card,
@@ -215,7 +215,14 @@ import {
215
215
  FileTree,
216
216
  RichFileTree,
217
217
  filterFileTree
218
- } from "./chunk-HJKCSXCH.js";
218
+ } from "./chunk-LELGOLFV.js";
219
+ import {
220
+ detectFileFormat,
221
+ fileExtension,
222
+ getCodeLanguage,
223
+ getFormatLabel,
224
+ getSyntaxLanguage
225
+ } from "./chunk-ZRVH3WCA.js";
219
226
  import {
220
227
  Tabs,
221
228
  TabsContent,
@@ -228,11 +235,11 @@ import {
228
235
  import "./chunk-5Z5ZYMOJ.js";
229
236
  import {
230
237
  Markdown
231
- } from "./chunk-CD53GZOM.js";
238
+ } from "./chunk-FJBTCTZM.js";
232
239
  import {
233
240
  CodeBlock,
234
241
  CopyButton
235
- } from "./chunk-66BNMOVT.js";
242
+ } from "./chunk-WUQDUBJG.js";
236
243
  import {
237
244
  cn
238
245
  } from "./chunk-RQHJBTEU.js";
@@ -465,6 +472,8 @@ export {
465
472
  cn,
466
473
  copyText,
467
474
  createAuthFetcher,
475
+ detectFileFormat,
476
+ fileExtension,
468
477
  filterFileTree,
469
478
  formatBytes,
470
479
  formatDuration,
@@ -472,9 +481,12 @@ export {
472
481
  getActiveSession,
473
482
  getAllActiveSessions,
474
483
  getAllProjectActivity,
484
+ getCodeLanguage,
485
+ getFormatLabel,
475
486
  getSessionsByActivity,
476
487
  getSessionsForNavbar,
477
488
  getSessionsForProject,
489
+ getSyntaxLanguage,
478
490
  getToolCategory,
479
491
  getToolDisplayMetadata,
480
492
  getToolErrorText,
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
- export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-DjXf8eOG.js';
3
+ export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-0kSpWMnf.js';
4
4
 
5
5
  interface MarkdownProps {
6
6
  children: string;
package/dist/markdown.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import "./chunk-5Z5ZYMOJ.js";
2
2
  import {
3
3
  Markdown
4
- } from "./chunk-CD53GZOM.js";
4
+ } from "./chunk-FJBTCTZM.js";
5
5
  import {
6
6
  CodeBlock,
7
7
  CopyButton
8
- } from "./chunk-66BNMOVT.js";
8
+ } from "./chunk-WUQDUBJG.js";
9
9
  import "./chunk-RQHJBTEU.js";
10
10
  export {
11
11
  CodeBlock,
package/dist/openui.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import "./chunk-RQGKSCEZ.js";
2
2
  import {
3
3
  OpenUIArtifactRenderer
4
- } from "./chunk-TMFOPHHN.js";
4
+ } from "./chunk-52Y3FMFI.js";
5
5
  import "./chunk-GYPQXTJU.js";
6
6
  import "./chunk-MKTSMWVD.js";
7
- import "./chunk-CD53GZOM.js";
8
- import "./chunk-66BNMOVT.js";
7
+ import "./chunk-FJBTCTZM.js";
8
+ import "./chunk-WUQDUBJG.js";
9
9
  import "./chunk-RQHJBTEU.js";
10
10
  export {
11
11
  OpenUIArtifactRenderer
@@ -14,7 +14,7 @@ import * as SwitchPrimitives from '@radix-ui/react-switch';
14
14
  import * as LabelPrimitive from '@radix-ui/react-label';
15
15
  export { Logo, LogoProps, TangleKnot } from '@tangle-network/brand';
16
16
  export { A as ArtifactPane, a as ArtifactPaneProps } from './artifact-pane-DvJyPWV4.js';
17
- export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-DjXf8eOG.js';
17
+ export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-0kSpWMnf.js';
18
18
 
19
19
  declare const Card: React$1.ForwardRefExoticComponent<React$1.HTMLAttributes<HTMLDivElement> & {
20
20
  variant?: "default" | "glass" | "sandbox" | "elevated";
@@ -100,7 +100,7 @@ import {
100
100
  import {
101
101
  CodeBlock,
102
102
  CopyButton
103
- } from "./chunk-66BNMOVT.js";
103
+ } from "./chunk-WUQDUBJG.js";
104
104
  import "./chunk-RQHJBTEU.js";
105
105
  export {
106
106
  ArtifactPane,
package/dist/run.js CHANGED
@@ -2,26 +2,27 @@ import "./chunk-LQS34IGP.js";
2
2
  import {
3
3
  ToolCallFeed,
4
4
  parseToolEvent
5
- } from "./chunk-7UO2ZMRQ.js";
5
+ } from "./chunk-5VPTNXX7.js";
6
6
  import {
7
7
  ExpandedToolDetail,
8
8
  InlineThinkingItem,
9
9
  InlineToolItem,
10
10
  LiveDuration,
11
11
  RunGroup
12
- } from "./chunk-YNN4O57I.js";
12
+ } from "./chunk-JBPWIYTQ.js";
13
13
  import {
14
14
  ToolCallGroup,
15
15
  ToolCallStep
16
- } from "./chunk-2VH6PUXD.js";
16
+ } from "./chunk-DGW77LD7.js";
17
17
  import "./chunk-4CLN43XT.js";
18
18
  import "./chunk-BX6AQMUS.js";
19
- import "./chunk-XIHMJ7ZQ.js";
20
- import "./chunk-TMFOPHHN.js";
19
+ import "./chunk-AAUNOHVL.js";
20
+ import "./chunk-52Y3FMFI.js";
21
21
  import "./chunk-GYPQXTJU.js";
22
22
  import "./chunk-MKTSMWVD.js";
23
- import "./chunk-CD53GZOM.js";
24
- import "./chunk-66BNMOVT.js";
23
+ import "./chunk-ZRVH3WCA.js";
24
+ import "./chunk-FJBTCTZM.js";
25
+ import "./chunk-WUQDUBJG.js";
25
26
  import "./chunk-RQHJBTEU.js";
26
27
  export {
27
28
  ExpandedToolDetail,
package/dist/sdk-hooks.js CHANGED
@@ -5,18 +5,18 @@ import {
5
5
  useSSEStream,
6
6
  useSdkSession,
7
7
  useToolCallStream
8
- } from "./chunk-YJ2G3XO5.js";
8
+ } from "./chunk-CMX2I43A.js";
9
9
  import "./chunk-OEX7NZE3.js";
10
10
  import {
11
11
  useAutoScroll,
12
12
  useRunCollapseState,
13
13
  useRunGroups
14
14
  } from "./chunk-54SQQMMM.js";
15
- import "./chunk-7UO2ZMRQ.js";
16
- import "./chunk-2VH6PUXD.js";
15
+ import "./chunk-5VPTNXX7.js";
16
+ import "./chunk-DGW77LD7.js";
17
17
  import "./chunk-BX6AQMUS.js";
18
- import "./chunk-CD53GZOM.js";
19
- import "./chunk-66BNMOVT.js";
18
+ import "./chunk-FJBTCTZM.js";
19
+ import "./chunk-WUQDUBJG.js";
20
20
  import "./chunk-RQHJBTEU.js";
21
21
  export {
22
22
  RealtimeSessionRegistry,
@@ -7,8 +7,9 @@ import {
7
7
  QuestionPreview,
8
8
  WebSearchPreview,
9
9
  WriteFilePreview
10
- } from "./chunk-XIHMJ7ZQ.js";
11
- import "./chunk-66BNMOVT.js";
10
+ } from "./chunk-AAUNOHVL.js";
11
+ import "./chunk-ZRVH3WCA.js";
12
+ import "./chunk-WUQDUBJG.js";
12
13
  import "./chunk-RQHJBTEU.js";
13
14
  export {
14
15
  CommandPreview,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -132,7 +132,7 @@
132
132
  "react": "^18 || ^19",
133
133
  "react-dom": "^18 || ^19",
134
134
  "react-router": "^7",
135
- "@tangle-network/brand": "^0.4.0"
135
+ "@tangle-network/brand": "^0.5.0"
136
136
  },
137
137
  "peerDependenciesMeta": {
138
138
  "@nanostores/react": {
@@ -6,6 +6,7 @@ import {
6
6
  type DocumentEditorPaneCollaborationConfig,
7
7
  } from "../editor/document-editor-pane";
8
8
  import { ArtifactPane, type ArtifactPaneProps } from "../primitives/artifact-pane";
9
+ import { detectFileFormat } from "./file-format";
9
10
  import { FilePreview, type FilePreviewProps } from "./file-preview";
10
11
  import { FileTabs, type FileTabData } from "./file-tabs";
11
12
 
@@ -79,9 +80,8 @@ export function FileArtifactPane({
79
80
  />
80
81
  ) : undefined;
81
82
  const isMarkdown =
82
- mimeType === "text/markdown" ||
83
- filename.toLowerCase().endsWith(".md") ||
84
- path?.toLowerCase().endsWith(".md");
83
+ detectFileFormat(filename, mimeType) === "markdown" ||
84
+ (path ? detectFileFormat(path, mimeType) === "markdown" : false);
85
85
  const isEditableMarkdown = isMarkdown && editor?.enabled;
86
86
  const headerActions = (
87
87
  <>
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ detectFileFormat,
4
+ fileExtension,
5
+ getCodeLanguage,
6
+ getFormatLabel,
7
+ getSyntaxLanguage,
8
+ } from "./file-format";
9
+
10
+ describe("detectFileFormat", () => {
11
+ it("detects by extension", () => {
12
+ expect(detectFileFormat("report.pdf")).toBe("pdf");
13
+ expect(detectFileFormat("photo.PNG")).toBe("image");
14
+ expect(detectFileFormat("data.csv")).toBe("csv");
15
+ expect(detectFileFormat("book.xlsx")).toBe("spreadsheet");
16
+ expect(detectFileFormat("legacy.xls")).toBe("spreadsheet");
17
+ expect(detectFileFormat("main.py")).toBe("code");
18
+ expect(detectFileFormat("app.tsx")).toBe("code");
19
+ expect(detectFileFormat("config.json")).toBe("json");
20
+ expect(detectFileFormat("compose.yaml")).toBe("yaml");
21
+ expect(detectFileFormat("compose.yml")).toBe("yaml");
22
+ expect(detectFileFormat("README.md")).toBe("markdown");
23
+ expect(detectFileFormat("notes.markdown")).toBe("markdown");
24
+ expect(detectFileFormat("output.log")).toBe("text");
25
+ });
26
+
27
+ it("treats shell dotfiles as code", () => {
28
+ expect(detectFileFormat(".bashrc")).toBe("code");
29
+ expect(detectFileFormat(".profile")).toBe("code");
30
+ expect(detectFileFormat(".gitignore")).toBe("code");
31
+ });
32
+
33
+ it("routes every highlightable extension to the code viewer", () => {
34
+ // detectFileFormat must stay in sync with getSyntaxLanguage — anything we
35
+ // can highlight (and that has no dedicated format) renders as code, not text.
36
+ for (const file of [
37
+ "main.rs",
38
+ "server.go",
39
+ "app.rb",
40
+ "styles.css",
41
+ "theme.scss",
42
+ "index.html",
43
+ "Cargo.toml",
44
+ "query.sql",
45
+ "Token.sol",
46
+ "schema.proto",
47
+ "module.mjs",
48
+ "legacy.cjs",
49
+ "deploy.zsh",
50
+ ]) {
51
+ expect(detectFileFormat(file)).toBe("code");
52
+ expect(getSyntaxLanguage(file)).toBeDefined();
53
+ }
54
+ });
55
+
56
+ it("keeps formats with a dedicated renderer out of the code viewer", () => {
57
+ expect(detectFileFormat("config.json")).toBe("json");
58
+ expect(detectFileFormat("compose.yml")).toBe("yaml");
59
+ expect(detectFileFormat("README.md")).toBe("markdown");
60
+ });
61
+
62
+ it("prefers MIME type when it is more specific", () => {
63
+ expect(detectFileFormat("file", "application/pdf")).toBe("pdf");
64
+ expect(detectFileFormat("blob", "image/png")).toBe("image");
65
+ expect(detectFileFormat("CHANGELOG", "text/markdown")).toBe("markdown");
66
+ expect(detectFileFormat("noext", "text/plain")).toBe("text");
67
+ });
68
+
69
+ it("falls back to unknown when nothing matches", () => {
70
+ expect(detectFileFormat("mystery.bin")).toBe("unknown");
71
+ expect(detectFileFormat("noextension")).toBe("unknown");
72
+ });
73
+
74
+ it("does not classify a dotless basename that happens to spell an extension", () => {
75
+ // A file literally named "pdf"/"json" has no extension — it must not be
76
+ // treated as that format (e.g. a pdf with no blobUrl renders empty).
77
+ expect(detectFileFormat("pdf")).toBe("unknown");
78
+ expect(detectFileFormat("json")).toBe("unknown");
79
+ expect(detectFileFormat("csv")).toBe("unknown");
80
+ });
81
+
82
+ it("lets a concrete extension win over a generic text/plain MIME", () => {
83
+ expect(detectFileFormat("config.json", "text/plain")).toBe("json");
84
+ expect(detectFileFormat("main.py", "text/plain")).toBe("code");
85
+ });
86
+
87
+ it("detects structured-data MIME types on generically-named files", () => {
88
+ expect(detectFileFormat("data", "application/json")).toBe("json");
89
+ expect(detectFileFormat("records", "text/csv")).toBe("csv");
90
+ expect(detectFileFormat("export", "application/csv")).toBe("csv");
91
+ expect(detectFileFormat("config", "application/yaml")).toBe("yaml");
92
+ expect(detectFileFormat("feed", "application/x-yaml")).toBe("yaml");
93
+ expect(detectFileFormat("feed", "text/yaml")).toBe("yaml");
94
+ });
95
+
96
+ it("ignores MIME charset parameters", () => {
97
+ expect(detectFileFormat("data", "application/json; charset=utf-8")).toBe("json");
98
+ expect(detectFileFormat("blob", "IMAGE/PNG")).toBe("image");
99
+ });
100
+
101
+ it("treats an authoritative MIME type as outranking a conflicting extension", () => {
102
+ expect(detectFileFormat("notes.json", "text/markdown")).toBe("markdown");
103
+ expect(detectFileFormat("page.txt", "application/json")).toBe("json");
104
+ });
105
+ });
106
+
107
+ describe("getFormatLabel", () => {
108
+ it("maps every format to a human label", () => {
109
+ expect(getFormatLabel("pdf")).toBe("PDF");
110
+ expect(getFormatLabel("json")).toBe("JSON");
111
+ expect(getFormatLabel("yaml")).toBe("YAML");
112
+ expect(getFormatLabel("markdown")).toBe("Markdown");
113
+ expect(getFormatLabel("spreadsheet")).toBe("Spreadsheet");
114
+ expect(getFormatLabel("unknown")).toBe("File");
115
+ });
116
+ });
117
+
118
+ describe("getSyntaxLanguage", () => {
119
+ it("maps known extensions to highlight.js language ids", () => {
120
+ expect(getSyntaxLanguage("main.py")).toBe("python");
121
+ expect(getSyntaxLanguage("server.ts")).toBe("typescript");
122
+ expect(getSyntaxLanguage("index.mjs")).toBe("javascript");
123
+ expect(getSyntaxLanguage("config.json")).toBe("json");
124
+ expect(getSyntaxLanguage("compose.yml")).toBe("yaml");
125
+ expect(getSyntaxLanguage(".bashrc")).toBe("bash");
126
+ expect(getSyntaxLanguage("lib.rs")).toBe("rust");
127
+ });
128
+
129
+ it("resolves from a full path, not just a basename", () => {
130
+ expect(getSyntaxLanguage("src/server/index.ts")).toBe("typescript");
131
+ });
132
+
133
+ it("returns undefined for unmapped extensions", () => {
134
+ expect(getSyntaxLanguage("mystery.bin")).toBeUndefined();
135
+ expect(getSyntaxLanguage("noextension")).toBeUndefined();
136
+ });
137
+ });
138
+
139
+ describe("getCodeLanguage", () => {
140
+ it("uses the detected format for json/yaml, covering extensionless MIME-only files", () => {
141
+ expect(getCodeLanguage("config", "json")).toBe("json");
142
+ expect(getCodeLanguage("config", "yaml")).toBe("yaml");
143
+ // A real extension resolves to the same answer.
144
+ expect(getCodeLanguage("config.json", "json")).toBe("json");
145
+ expect(getCodeLanguage("compose.yml", "yaml")).toBe("yaml");
146
+ });
147
+
148
+ it("keys off the extension for other code formats", () => {
149
+ expect(getCodeLanguage("main.rs", "code")).toBe("rust");
150
+ expect(getCodeLanguage("server.ts", "code")).toBe("typescript");
151
+ expect(getCodeLanguage("notes", "code")).toBeUndefined();
152
+ });
153
+ });
154
+
155
+ describe("fileExtension", () => {
156
+ it("lowercases the trailing extension", () => {
157
+ expect(fileExtension("Photo.JPEG")).toBe("jpeg");
158
+ });
159
+
160
+ it("returns the dotfile name for files that are all extension", () => {
161
+ expect(fileExtension(".gitignore")).toBe("gitignore");
162
+ });
163
+
164
+ it("ignores dots in directory components", () => {
165
+ expect(fileExtension("my.config.dir/file")).toBe("");
166
+ expect(fileExtension("v1.2.0/Makefile")).toBe("");
167
+ expect(fileExtension("a.b.c/server.ts")).toBe("ts");
168
+ });
169
+
170
+ it("returns no extension for a dotless basename, even one that looks like an extension", () => {
171
+ expect(fileExtension("json")).toBe("");
172
+ expect(fileExtension("pdf")).toBe("");
173
+ expect(fileExtension("README")).toBe("");
174
+ expect(fileExtension("Makefile")).toBe("");
175
+ });
176
+ });