@tooee/view 0.7.3 → 0.9.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 (106) hide show
  1. package/dist/components/subviews/code-subview.d.ts +2 -1
  2. package/dist/components/subviews/code-subview.d.ts.map +1 -1
  3. package/dist/components/subviews/code-subview.js +2 -15
  4. package/dist/components/subviews/code-subview.js.map +1 -1
  5. package/dist/components/subviews/custom-subview.d.ts +2 -1
  6. package/dist/components/subviews/custom-subview.d.ts.map +1 -1
  7. package/dist/components/subviews/custom-subview.js +3 -13
  8. package/dist/components/subviews/custom-subview.js.map +1 -1
  9. package/dist/components/subviews/diff-subview.d.ts +2 -1
  10. package/dist/components/subviews/diff-subview.d.ts.map +1 -1
  11. package/dist/components/subviews/diff-subview.js +23 -33
  12. package/dist/components/subviews/diff-subview.js.map +1 -1
  13. package/dist/components/subviews/image-subview.d.ts +2 -1
  14. package/dist/components/subviews/image-subview.d.ts.map +1 -1
  15. package/dist/components/subviews/image-subview.js +4 -9
  16. package/dist/components/subviews/image-subview.js.map +1 -1
  17. package/dist/components/subviews/markdown-subview.d.ts +2 -1
  18. package/dist/components/subviews/markdown-subview.d.ts.map +1 -1
  19. package/dist/components/subviews/markdown-subview.js +8 -15
  20. package/dist/components/subviews/markdown-subview.js.map +1 -1
  21. package/dist/components/subviews/table-subview.d.ts +2 -1
  22. package/dist/components/subviews/table-subview.d.ts.map +1 -1
  23. package/dist/components/subviews/table-subview.js +9 -48
  24. package/dist/components/subviews/table-subview.js.map +1 -1
  25. package/dist/components/view-state.d.ts +2 -1
  26. package/dist/components/view-state.d.ts.map +1 -1
  27. package/dist/components/view-state.js.map +1 -1
  28. package/dist/content-loader-store.d.ts +3 -2
  29. package/dist/content-loader-store.d.ts.map +1 -1
  30. package/dist/content-loader-store.js +5 -4
  31. package/dist/content-loader-store.js.map +1 -1
  32. package/dist/default-provider.d.ts.map +1 -1
  33. package/dist/default-provider.js +29 -29
  34. package/dist/default-provider.js.map +1 -1
  35. package/dist/directory-view.d.ts +2 -1
  36. package/dist/directory-view.d.ts.map +1 -1
  37. package/dist/directory-view.js +2 -1
  38. package/dist/directory-view.js.map +1 -1
  39. package/dist/hooks/use-content-commands.d.ts +5 -3
  40. package/dist/hooks/use-content-commands.d.ts.map +1 -1
  41. package/dist/hooks/use-content-commands.js.map +1 -1
  42. package/dist/hooks/use-content-document.d.ts +28 -0
  43. package/dist/hooks/use-content-document.d.ts.map +1 -0
  44. package/dist/hooks/use-content-document.js +21 -0
  45. package/dist/hooks/use-content-document.js.map +1 -0
  46. package/dist/hooks/use-content-loader.d.ts +10 -6
  47. package/dist/hooks/use-content-loader.d.ts.map +1 -1
  48. package/dist/hooks/use-content-loader.js +1 -1
  49. package/dist/hooks/use-content-loader.js.map +1 -1
  50. package/dist/hooks/use-view-command-context.d.ts +4 -2
  51. package/dist/hooks/use-view-command-context.d.ts.map +1 -1
  52. package/dist/hooks/use-view-command-context.js +6 -1
  53. package/dist/hooks/use-view-command-context.js.map +1 -1
  54. package/dist/index.d.ts +4 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/launch.d.ts +5 -0
  59. package/dist/launch.d.ts.map +1 -1
  60. package/dist/launch.js +15 -4
  61. package/dist/launch.js.map +1 -1
  62. package/dist/link-handlers.d.ts +21 -0
  63. package/dist/link-handlers.d.ts.map +1 -0
  64. package/dist/link-handlers.js +35 -0
  65. package/dist/link-handlers.js.map +1 -0
  66. package/dist/markdown-links.d.ts +21 -0
  67. package/dist/markdown-links.d.ts.map +1 -0
  68. package/dist/markdown-links.js +74 -0
  69. package/dist/markdown-links.js.map +1 -0
  70. package/dist/standalone-router.d.ts +11 -0
  71. package/dist/standalone-router.d.ts.map +1 -0
  72. package/dist/standalone-router.js +85 -0
  73. package/dist/standalone-router.js.map +1 -0
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +3 -33
  76. package/dist/types.js.map +1 -1
  77. package/dist/view.d.ts +2 -1
  78. package/dist/view.d.ts.map +1 -1
  79. package/dist/view.js.map +1 -1
  80. package/package.json +14 -11
  81. package/src/components/subviews/code-subview.tsx +8 -20
  82. package/src/components/subviews/custom-subview.tsx +9 -19
  83. package/src/components/subviews/diff-subview.tsx +35 -43
  84. package/src/components/subviews/image-subview.tsx +12 -11
  85. package/src/components/subviews/markdown-subview.tsx +16 -22
  86. package/src/components/subviews/table-subview.tsx +16 -56
  87. package/src/components/view-state.tsx +2 -1
  88. package/src/content-loader-store.ts +8 -5
  89. package/src/default-provider.ts +34 -32
  90. package/src/directory-view.tsx +4 -2
  91. package/src/hooks/use-content-commands.ts +6 -1
  92. package/src/hooks/use-content-document.ts +54 -0
  93. package/src/hooks/use-content-loader.ts +17 -3
  94. package/src/hooks/use-view-command-context.ts +16 -4
  95. package/src/index.ts +4 -0
  96. package/src/launch.tsx +27 -10
  97. package/src/link-handlers.ts +60 -0
  98. package/src/markdown-links.ts +88 -0
  99. package/src/standalone-router.tsx +122 -0
  100. package/src/types.ts +4 -35
  101. package/src/view.tsx +2 -1
  102. package/dist/hooks/index.d.ts +0 -5
  103. package/dist/hooks/index.d.ts.map +0 -1
  104. package/dist/hooks/index.js +0 -4
  105. package/dist/hooks/index.js.map +0 -1
  106. package/src/hooks/index.ts +0 -11
@@ -0,0 +1,88 @@
1
+ import { statSync } from "node:fs";
2
+ import nodePath from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Lexer } from "marked";
5
+ import type { Token, Tokens } from "marked";
6
+
7
+ export type LinkResolution =
8
+ | { status: "file"; path: string }
9
+ | { status: "missing"; path: string }
10
+ | { status: "directory"; path: string }
11
+ | { status: "unsupported" };
12
+
13
+ /** Existing local files only. Fragments are validated, then open at the file top. */
14
+ export const resolveMarkdownLink = function resolveMarkdownLink(
15
+ href: string,
16
+ baseDir: string,
17
+ currentPath?: string,
18
+ ): LinkResolution {
19
+ try {
20
+ const decoded = decodeURIComponent(href);
21
+ if (/[\p{Cc}\\]/u.test(decoded)) {
22
+ return { status: "unsupported" };
23
+ }
24
+ const target = href.trim();
25
+ if (!target || target.startsWith("//")) {
26
+ return { status: "unsupported" };
27
+ }
28
+ const [rawPath] = target.split("#", 1);
29
+ if (rawPath.includes("?")) {
30
+ return { status: "unsupported" };
31
+ }
32
+ let path: string;
33
+ if (/^[a-zA-Z][a-zA-Z\d+.-]*:/u.test(rawPath)) {
34
+ // Check the authority before URL normalizes localhost into an empty host.
35
+ if (!/^file:\/\/\//iu.test(rawPath)) {
36
+ return { status: "unsupported" };
37
+ }
38
+ const url = new URL(rawPath);
39
+ if (url.hostname || url.search) {
40
+ return { status: "unsupported" };
41
+ }
42
+ path = fileURLToPath(url);
43
+ } else {
44
+ path = rawPath
45
+ ? nodePath.resolve(baseDir, decodeURIComponent(rawPath))
46
+ : (currentPath ?? baseDir);
47
+ }
48
+ const absolute = nodePath.resolve(path);
49
+ try {
50
+ const stat = statSync(absolute);
51
+ if (stat.isFile()) {
52
+ return { path: absolute, status: "file" };
53
+ }
54
+ return stat.isDirectory()
55
+ ? { path: absolute, status: "directory" }
56
+ : { status: "unsupported" };
57
+ } catch {
58
+ return { path: absolute, status: "missing" };
59
+ }
60
+ } catch {
61
+ return { status: "unsupported" };
62
+ }
63
+ };
64
+
65
+ export interface MarkdownLink {
66
+ /** Link text as written in the source. */
67
+ text: string;
68
+ href: string;
69
+ }
70
+
71
+ /** Ordinary inline links in source order, skipping images and code spans. */
72
+ const collect = (tokens: Token[], links: MarkdownLink[]): void => {
73
+ for (const token of tokens) {
74
+ if (token.type === "link" && "href" in token) {
75
+ // SAFETY: Marked's Generic token prevents discriminator narrowing.
76
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- checked Marked link discriminator
77
+ const { href, text } = token as Tokens.Link;
78
+ links.push({ href, text });
79
+ } else if (["strong", "em", "del"].includes(token.type) && "tokens" in token && token.tokens) {
80
+ collect(token.tokens, links);
81
+ }
82
+ }
83
+ };
84
+ export const markdownLinks = function markdownLinks(line: string): MarkdownLink[] {
85
+ const links: MarkdownLink[] = [];
86
+ collect(Lexer.lexInline(line), links);
87
+ return links;
88
+ };
@@ -0,0 +1,122 @@
1
+ /* oxlint-disable no-use-before-define -- the screen closes over the route initialized before router startup */
2
+ import { useMemo } from "react";
3
+ import type { ReactNode } from "react";
4
+ import { useChooseDialog } from "@tooee/choose";
5
+ import { useCommand } from "@tooee/commands";
6
+ import type { CommandContext } from "@tooee/commands";
7
+ import {
8
+ createRoute,
9
+ createRouter,
10
+ useNavigate,
11
+ useParams,
12
+ useRouter,
13
+ useRouterCommands,
14
+ } from "@tooee/router";
15
+ import type { Codec, RouteDefinition, RouterInstance } from "@tooee/router";
16
+ import path from "node:path";
17
+ import { createFileProvider } from "./default-provider.js";
18
+ import { runLinkHandlers } from "./link-handlers.js";
19
+ import { markdownLinks } from "./markdown-links.js";
20
+ import type { MarkdownLink } from "./markdown-links.js";
21
+ import { getTextContent } from "./types.js";
22
+ import type { ViewLaunchOptions } from "./launch.js";
23
+ import { View } from "./view.js";
24
+
25
+ const documentParams: Codec<{ path?: string }> = {
26
+ parse: (value) => {
27
+ /* oxlint-disable anti-slop/no-runtime-typeof -- route params are decoded at this boundary */
28
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
29
+ throw new TypeError("Expected document params");
30
+ }
31
+ if (!("path" in value) || value.path === undefined) {
32
+ return {};
33
+ }
34
+ if (typeof value.path !== "string" || !path.isAbsolute(value.path)) {
35
+ throw new TypeError("Expected an absolute document path");
36
+ }
37
+ /* oxlint-enable anti-slop/no-runtime-typeof */
38
+ return { path: value.path };
39
+ },
40
+ };
41
+
42
+ interface StandaloneRouter {
43
+ documentRoute: RouteDefinition<{ path?: string }>;
44
+ router: RouterInstance;
45
+ }
46
+
47
+ export const createStandaloneRouter = function createStandaloneRouter(
48
+ options: ViewLaunchOptions,
49
+ ): StandaloneRouter {
50
+ const DocumentScreen = function DocumentScreen(): ReactNode {
51
+ useRouterCommands();
52
+ const { path: documentPath } = useParams(documentRoute);
53
+ const navigate = useNavigate();
54
+ const { stack } = useRouter();
55
+ const chooseLink = useChooseDialog<MarkdownLink>();
56
+ const provider = useMemo(
57
+ () =>
58
+ documentPath === undefined ? options.contentProvider : createFileProvider(documentPath),
59
+ [documentPath],
60
+ );
61
+ const activate = (href: string, command: CommandContext): boolean => {
62
+ if (options.filePath === undefined) {
63
+ command.toast?.toast({ level: "info", message: "Links need a source file" });
64
+ return true;
65
+ }
66
+ const currentPath = path.resolve(documentPath ?? options.filePath);
67
+ return runLinkHandlers(
68
+ options.linkHandlers ?? [],
69
+ { baseDir: path.dirname(currentPath), currentPath, href },
70
+ { command, documentRoute, navigate },
71
+ );
72
+ };
73
+ useCommand({
74
+ handler: async (context) => {
75
+ // Source positions are zero-based, unlike the displayed line numbers.
76
+ const line = context.document?.activeAnchor?.source?.primary?.start.line;
77
+ const content = context.view?.content;
78
+ if (line === undefined || content === undefined) {
79
+ return;
80
+ }
81
+ const links = markdownLinks(getTextContent(content).split("\n")[line] ?? "");
82
+ // One link follows directly; several open a chooser so none is silently preferred.
83
+ const link =
84
+ links.length > 1
85
+ ? await chooseLink.open({
86
+ items: links,
87
+ prompt: "Follow link",
88
+ toItem: (item) => ({ description: item.href, text: item.text }),
89
+ })
90
+ : (links[0] ?? null);
91
+ if (link !== null) {
92
+ activate(link.href, context);
93
+ }
94
+ },
95
+ hotkey: "enter",
96
+ id: "view.follow-link",
97
+ modes: ["cursor"],
98
+ title: "Follow Markdown link",
99
+ when: (context) => context.view?.format === "markdown",
100
+ });
101
+ return (
102
+ <View
103
+ key={`${stack.length}:${documentPath ?? ""}`}
104
+ actions={options.actions}
105
+ codeBlockRenderers={options.codeBlockRenderers}
106
+ contentProvider={provider}
107
+ onMarkdownLinkActivate={activate}
108
+ renderers={options.renderers}
109
+ />
110
+ );
111
+ };
112
+ const documentRoute = createRoute({
113
+ component: DocumentScreen,
114
+ id: "view.document",
115
+ params: documentParams,
116
+ });
117
+ const router = createRouter({
118
+ initial: { routeId: documentRoute.id },
119
+ routes: [documentRoute],
120
+ });
121
+ return { documentRoute, router };
122
+ };
package/src/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { ImageFit, ImageRenderProtocol, ImageSource } from "@opentui/core";
3
3
  import type { CommandContext } from "@tooee/commands";
4
+ import { formatTableCell } from "@tooee/renderers";
4
5
  import type { ColumnDef, SourceLineRow, TableRow } from "@tooee/renderers";
5
6
  import type { DocumentController } from "@tooee/shell";
6
7
  import type { MarkSet } from "@tooee/marks";
@@ -18,6 +19,7 @@ export type Content =
18
19
  export type ContentFormat = Content["format"];
19
20
 
20
21
  /** Handles an inline Markdown link with the View's live command context. */
22
+ // oxlint-disable-next-line anti-slop/no-unknown-returns -- public callback contract preserves the renderer's exact-true link-consumption result and permits legacy return values
21
23
  export type MarkdownLinkActivateHandler = (href: string, context: CommandContext) => unknown;
22
24
 
23
25
  interface BaseContent {
@@ -152,40 +154,6 @@ export const isCustomContent = function isCustomContent(
152
154
  return !BUILTIN_FORMATS.has(content.format);
153
155
  };
154
156
 
155
- const stringifyCell = function stringifyCell(value: unknown): string {
156
- if (value === null || value === undefined) {
157
- return "";
158
- }
159
- if (typeof value === "string") {
160
- return value;
161
- }
162
- if (typeof value === "number" || typeof value === "boolean") {
163
- return String(value);
164
- }
165
- if (typeof value === "string") {
166
- return value;
167
- }
168
- if (
169
- typeof value === "number" ||
170
- typeof value === "bigint" ||
171
- typeof value === "boolean" ||
172
- typeof value === "symbol"
173
- ) {
174
- return value.toString();
175
- }
176
- if (value === null || value === undefined) {
177
- return String(value);
178
- }
179
- if (typeof value === "function") {
180
- return Function.prototype.toString.call(value);
181
- }
182
- try {
183
- return JSON.stringify(value);
184
- } catch {
185
- return Array.isArray(value) ? value.join(",") : Object.prototype.toString.call(value);
186
- }
187
- };
188
-
189
157
  export const getTextContent = function getTextContent(content: AnyContent): string {
190
158
  if (content.getTextContent) {
191
159
  return content.getTextContent();
@@ -209,6 +177,7 @@ export const getTextContent = function getTextContent(content: AnyContent): stri
209
177
  return content.text;
210
178
  }
211
179
  case "image": {
180
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- ImageSource is OpenTUI's public string-or-binary union; only its string and URL variants have text content
212
181
  if (typeof content.src === "string") {
213
182
  return content.src;
214
183
  }
@@ -217,7 +186,7 @@ export const getTextContent = function getTextContent(content: AnyContent): stri
217
186
  case "table": {
218
187
  const headers = content.columns.map((column) => column.header ?? column.key);
219
188
  const rowLines = content.rows.map((row) =>
220
- content.columns.map((column) => stringifyCell(row[column.key])).join("\t"),
189
+ content.columns.map((column) => formatTableCell(row[column.key])).join("\t"),
221
190
  );
222
191
  return [headers.join("\t"), ...rowLines].join("\n");
223
192
  }
package/src/view.tsx CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  DiffSubview,
17
17
  CustomSubview,
18
18
  } from "./components/subviews/index.js";
19
+ import type { ReactNode } from "react";
19
20
 
20
21
  interface ViewProps {
21
22
  contentProvider: ContentProvider;
@@ -36,7 +37,7 @@ export const View = function View({
36
37
  renderers,
37
38
  codeBlockRenderers,
38
39
  onMarkdownLinkActivate,
39
- }: ViewProps): React.ReactNode {
40
+ }: ViewProps): ReactNode {
40
41
  const { theme } = useTheme();
41
42
  const { content, streaming, error, providerMarks, reload } = useContentLoader(contentProvider);
42
43
 
@@ -1,5 +0,0 @@
1
- export { useContentLoader } from "./use-content-loader.js";
2
- export { useContentCommands } from "./use-content-commands.js";
3
- export { createViewCommandContext, useProvideViewCommandContext, } from "./use-view-command-context.js";
4
- export type { CreateViewCommandContextOptions, ProvideViewCommandContextOptions, ViewCommandContext, } from "./use-view-command-context.js";
5
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EACL,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,+BAA+B,EAC/B,gCAAgC,EAChC,kBAAkB,GACnB,MAAM,+BAA+B,CAAC"}
@@ -1,4 +0,0 @@
1
- export { useContentLoader } from "./use-content-loader.js";
2
- export { useContentCommands } from "./use-content-commands.js";
3
- export { createViewCommandContext, useProvideViewCommandContext, } from "./use-view-command-context.js";
4
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EACL,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,+BAA+B,CAAC"}
@@ -1,11 +0,0 @@
1
- export { useContentLoader } from "./use-content-loader.js";
2
- export { useContentCommands } from "./use-content-commands.js";
3
- export {
4
- createViewCommandContext,
5
- useProvideViewCommandContext,
6
- } from "./use-view-command-context.js";
7
- export type {
8
- CreateViewCommandContextOptions,
9
- ProvideViewCommandContextOptions,
10
- ViewCommandContext,
11
- } from "./use-view-command-context.js";