paseo-ratex-markdown 0.1.0 → 0.1.2
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/OVERVIEW.md +4 -4
- package/README.md +18 -9
- package/client/active-file.ts +86 -0
- package/client/workspace-panel.tsx +19 -6
- package/index.client.tsx +6 -2
- package/npm-shrinkwrap.json +4192 -0
- package/package.json +2 -1
- package/scripts/embed-assets.mjs +56 -9
- package/scripts/prepare-plugin.mjs +6 -6
- package/server/glyphs.ts +16 -43
- package/server/vendor/katex-glyphs.ts +45 -0
- package/server/vendor/katex-fonts.ts +0 -45
package/OVERVIEW.md
CHANGED
|
@@ -2,21 +2,21 @@ RaTeX Markdown renders Markdown files with their LaTeX math typeset by RaTeX, a
|
|
|
2
2
|
|
|
3
3
|
## Opening a file
|
|
4
4
|
|
|
5
|
-
On Paseo 0.13 and later, opening a `.md` or `.markdown` file offers **Markdown (RaTeX)** alongside Paseo's own Markdown and Source panels, and **Don't ask me again** makes it the default. On Paseo 0.11 and 0.12, which have no file panels, run **Open Markdown preview (RaTeX)** from the Command Center
|
|
5
|
+
On Paseo 0.13 and later, opening a `.md` or `.markdown` file offers **Markdown (RaTeX)** alongside Paseo's own Markdown and Source panels, and **Don't ask me again** makes it the default. On Paseo 0.11 and 0.12, which have no file panels, open a Markdown file and run **Open Markdown preview (RaTeX)** from the Command Center: the panel shows that file. Run from any other tab, it lists the workspace's Markdown files to pick from. The open document follows edits on disk.
|
|
6
6
|
|
|
7
7
|
## How it works
|
|
8
8
|
|
|
9
|
-
The daemon lays out each formula with the bundled `ratex-wasm` module and
|
|
9
|
+
The daemon lays out each formula with the bundled `ratex-wasm` module and uses embedded KaTeX glyph outlines, so nothing is downloaded at runtime. The app draws each formula as a vector image that follows the Paseo theme. Selecting text across a formula copies its TeX source. A formula RaTeX cannot parse is shown as source with the error.
|
|
10
10
|
|
|
11
11
|
## Setup and requirements
|
|
12
12
|
|
|
13
|
-
Paseo 0.11.0 or later. Installing from npm or Git needs Node.js and npm on the daemon host
|
|
13
|
+
Paseo 0.11.0 or later. Installing from npm or Git needs Node.js and npm on the daemon host. The npm package includes its embedded math engine and glyph outlines; a Git installation prepares them from locked dependencies.
|
|
14
14
|
|
|
15
15
|
Desktop and web clients render formulas. iOS and Android clients render them where Paseo draws inline SVG icons (0.13 and later) and show the TeX source otherwise.
|
|
16
16
|
|
|
17
17
|
## Data access
|
|
18
18
|
|
|
19
|
-
The daemon reads only Markdown and image files inside the directory of the workspace being viewed, after resolving symlinks, and refuses paths outside it. It makes no network requests. Images with `http` or `https` addresses in a document are loaded by the app directly. Links open in the system browser only for `http`, `https` and `mailto` addresses. Raw HTML in a document is never interpreted: HTML blocks show as source, and inline tags are either dropped with their text kept (formatting such as `<kbd>`) or shown as written.
|
|
19
|
+
The daemon reads only Markdown and image files inside the directory of the workspace being viewed, after resolving symlinks, and refuses paths outside it. It makes no network requests. Images with `http` or `https` addresses in a document are loaded by the app directly. Links open in the system browser only for `http`, `https` and `mailto` addresses. Raw HTML in a document is never interpreted: HTML blocks show as source, and inline tags are either dropped with their text kept (formatting such as `<kbd>`) or shown as written. On Paseo 0.11 and 0.12 the preview command reads the tab layout the app keeps in its local storage on web and desktop, only to find the open Markdown file.
|
|
20
20
|
|
|
21
21
|
## Limitations
|
|
22
22
|
|
package/README.md
CHANGED
|
@@ -31,9 +31,11 @@ Paseo's plugin API decides where the plugin can draw:
|
|
|
31
31
|
after 0.12.0): opening a
|
|
32
32
|
`.md` or `.markdown` file offers **Markdown (RaTeX)** next to Paseo's own Markdown and Source
|
|
33
33
|
panels. Pick it and check **Don't ask me again** to make it the default for Markdown files.
|
|
34
|
-
- **Paseo 0.11 and 0.12**: press <kbd>⌘K</kbd> / <kbd>Ctrl+K</kbd>
|
|
35
|
-
**Open Markdown preview (RaTeX)
|
|
36
|
-
|
|
34
|
+
- **Paseo 0.11 and 0.12**: open a Markdown file, press <kbd>⌘K</kbd> / <kbd>Ctrl+K</kbd> and
|
|
35
|
+
run **Open Markdown preview (RaTeX)**: the panel shows that file, and running the command again
|
|
36
|
+
from another Markdown file switches it. Run from any other tab, the panel lists the workspace's
|
|
37
|
+
Markdown files; pick one to read it. Relative links to other Markdown files open in the same
|
|
38
|
+
panel.
|
|
37
39
|
|
|
38
40
|
The plugin detects file panel support at runtime, so the same release works on both.
|
|
39
41
|
|
|
@@ -93,15 +95,17 @@ Markdown text ──► markdown-it + math rules (client) ──► document tre
|
|
|
93
95
|
│ formulas, batched
|
|
94
96
|
▼
|
|
95
97
|
daemon: RaTeX WASM lays out each formula (DisplayList)
|
|
96
|
-
|
|
98
|
+
precomputed KaTeX glyph outlines
|
|
97
99
|
│ scene: glyphs, rules, paths
|
|
98
100
|
▼
|
|
99
101
|
web/desktop: one SVG <img> per formula iOS/Android: one inline SVG icon per glyph
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
The daemon half embeds the `ratex-wasm` binary and
|
|
103
|
-
runtime.
|
|
104
|
-
|
|
104
|
+
The daemon half embeds the `ratex-wasm` binary and KaTeX glyph outlines, so nothing is fetched at
|
|
105
|
+
runtime. During preparation, opentype.js extracts the outlines from the pinned fonts; the
|
|
106
|
+
runtime reads those paths directly without a font parser. Its SVG output matches RaTeX's own
|
|
107
|
+
`ratex-svg` renderer pixel for pixel on the test formulas. Layouts are cached on both sides,
|
|
108
|
+
so a theme change repaints without new requests,
|
|
105
109
|
and an edit only lays out the formulas that changed.
|
|
106
110
|
|
|
107
111
|
## Privacy and data access
|
|
@@ -110,6 +114,8 @@ and an edit only lays out the formulas that changed.
|
|
|
110
114
|
directory that Paseo reports for the open workspace, after resolving symlinks. Paths that
|
|
111
115
|
leave the workspace are refused.
|
|
112
116
|
- Formulas are laid out on the daemon; nothing is sent to other services.
|
|
117
|
+
- To find the open Markdown file on Paseo 0.11 and 0.12, the command reads the tab layout the
|
|
118
|
+
app keeps in its local storage on web and desktop. It reads nothing else there and writes nothing.
|
|
113
119
|
- Images with `http(s)` addresses load directly on the client, as in any Markdown viewer.
|
|
114
120
|
- Links open in the system browser only for `http`, `https` and `mailto`.
|
|
115
121
|
- Raw HTML is never interpreted. HTML blocks show as source; inline formatting tags such as
|
|
@@ -124,7 +130,9 @@ and an edit only lays out the formulas that changed.
|
|
|
124
130
|
- Characters outside the KaTeX fonts, such as CJK inside `\text{}`, are drawn with the
|
|
125
131
|
client's system fonts.
|
|
126
132
|
- Paseo 0.12 and earlier have no file panels, so there documents open in the plugin's own
|
|
127
|
-
panel instead of the file tab.
|
|
133
|
+
panel instead of the file tab. Their plugin API does not say which file is open either: the
|
|
134
|
+
command reads the app's saved layout, which iOS and Android keep out of reach, so there it
|
|
135
|
+
always shows the file list.
|
|
128
136
|
|
|
129
137
|
## Development
|
|
130
138
|
|
|
@@ -148,7 +156,8 @@ Set `CHROME_CDP_URL` instead of `CHROME_BIN` to use a running Chrome, `RATEX_SCR
|
|
|
148
156
|
to save screenshots, and `RATEX_CLIENT_BUNDLE=file` to test a bundle taken from a daemon's
|
|
149
157
|
plugin catalog.
|
|
150
158
|
|
|
151
|
-
`npm run check:package` checks the files `npm pack` would publish
|
|
159
|
+
`npm run check:package` checks the files `npm pack` would publish, including the size of
|
|
160
|
+
reachable runtime dependencies and declarations against the registry's 2 MB budget.
|
|
152
161
|
|
|
153
162
|
## Credits
|
|
154
163
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown file open in Paseo's own file tab, so the preview command can show it.
|
|
3
|
+
*
|
|
4
|
+
* Paseo 0.11 and 0.12 tell plugins nothing about open tabs. On web and desktop the app persists
|
|
5
|
+
* its workspace layout in localStorage, and plugin code runs in the app's page, so this reads that
|
|
6
|
+
* private state. Anything it does not recognize means no file: the panel then keeps its own.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type KeyValueStorage = { getItem(key: string): string | null };
|
|
10
|
+
|
|
11
|
+
const LAYOUT_STORAGE_KEY = "workspace-layout-state";
|
|
12
|
+
|
|
13
|
+
type StoredTab = { tabId?: unknown; createdAt?: unknown; target?: { kind?: unknown; path?: unknown } };
|
|
14
|
+
type StoredPane = { id?: unknown; focusedTabId?: unknown; tabs?: unknown };
|
|
15
|
+
type StoredNode = { kind?: unknown; pane?: StoredPane; group?: { children?: unknown } };
|
|
16
|
+
type StoredLayout = { focusedPaneId?: unknown; root?: StoredNode };
|
|
17
|
+
|
|
18
|
+
/** The app's localStorage, or null on native clients and where storage is blocked. */
|
|
19
|
+
export function hostStorage(): KeyValueStorage | null {
|
|
20
|
+
try {
|
|
21
|
+
const storage = (globalThis as { localStorage?: KeyValueStorage }).localStorage;
|
|
22
|
+
return typeof storage?.getItem === "function" ? storage : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The workspace-relative path of the Markdown file in the focused tab. When focus sits elsewhere,
|
|
30
|
+
* such as the file tree, the newest Markdown file showing in any pane.
|
|
31
|
+
*/
|
|
32
|
+
export function focusedMarkdownFile(
|
|
33
|
+
storage: KeyValueStorage | null,
|
|
34
|
+
workspace: { id: string; directory: string },
|
|
35
|
+
): string | null {
|
|
36
|
+
for (const layout of storedLayouts(storage, workspace.id)) {
|
|
37
|
+
const panes = collectPanes(layout.root);
|
|
38
|
+
const focused = panes.find((pane) => pane.id === layout.focusedPaneId);
|
|
39
|
+
const shown = panes.map(visibleTab).filter((tab): tab is StoredTab => markdownPath(tab) !== null);
|
|
40
|
+
shown.sort((a, b) => createdAt(b) - createdAt(a));
|
|
41
|
+
const path = markdownPath(focused && visibleTab(focused)) ?? markdownPath(shown[0]);
|
|
42
|
+
if (path) return workspaceRelative(path, workspace.directory);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Layouts are keyed `serverId:workspaceId`; the plugin knows only the workspace. */
|
|
48
|
+
function storedLayouts(storage: KeyValueStorage | null, workspaceId: string): StoredLayout[] {
|
|
49
|
+
let state: unknown;
|
|
50
|
+
try {
|
|
51
|
+
state = JSON.parse(storage?.getItem(LAYOUT_STORAGE_KEY) ?? "null")?.state;
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const layouts = (state as { layoutByWorkspace?: unknown } | null)?.layoutByWorkspace;
|
|
56
|
+
if (!layouts || typeof layouts !== "object") return [];
|
|
57
|
+
return Object.entries(layouts)
|
|
58
|
+
.filter(([key, layout]) => key.endsWith(`:${workspaceId}`) && layout && typeof layout === "object")
|
|
59
|
+
.map(([, layout]) => layout as StoredLayout);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function collectPanes(node: StoredNode | undefined): StoredPane[] {
|
|
63
|
+
if (node?.kind === "pane" && node.pane) return [node.pane];
|
|
64
|
+
const children = node?.kind === "group" ? node.group?.children : null;
|
|
65
|
+
return Array.isArray(children) ? children.flatMap((child: StoredNode) => collectPanes(child)) : [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function visibleTab(pane: StoredPane): StoredTab | undefined {
|
|
69
|
+
return Array.isArray(pane.tabs) ? pane.tabs.find((tab: StoredTab) => tab?.tabId === pane.focusedTabId) : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function markdownPath(tab: StoredTab | undefined): string | null {
|
|
73
|
+
const path = tab?.target?.kind === "file" ? tab.target.path : null;
|
|
74
|
+
return typeof path === "string" && /\.(md|markdown)$/i.test(path) ? path : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createdAt(tab: StoredTab): number {
|
|
78
|
+
return typeof tab.createdAt === "number" ? tab.createdAt : 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** File tabs hold workspace-relative paths, but an absolute one inside the workspace is trimmed too. */
|
|
82
|
+
function workspaceRelative(file: string, directory: string): string {
|
|
83
|
+
const path = file.replace(/\\/g, "/");
|
|
84
|
+
const root = directory.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
85
|
+
return root && path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
|
|
86
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
|
2
2
|
import { openExternalUrl, useRpc } from "@getpaseo/plugin/client";
|
|
3
3
|
import { FlatList, Icon, Modal, TextInput } from "@getpaseo/plugin/client/react-native";
|
|
4
4
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
-
import { useCallback, useMemo, useState } from "react";
|
|
5
|
+
import { useCallback, useMemo, useState, useSyncExternalStore } from "react";
|
|
6
6
|
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
|
|
7
7
|
import { listMarkdownFilesRpc, readImageRpc, readMarkdownFileRpc } from "../shared/contracts";
|
|
8
8
|
import { MarkdownDocument } from "./document";
|
|
@@ -12,8 +12,22 @@ import { isExternalLink, resolveMarkdownLink } from "./links";
|
|
|
12
12
|
/** How often an open document checks the disk; unchanged files are not sent again. */
|
|
13
13
|
const POLL_INTERVAL = 2000;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The file each workspace shows, outside the panel: reopening the panel returns to it, and the
|
|
17
|
+
* command can switch a panel that is already open.
|
|
18
|
+
*/
|
|
19
|
+
const shownFiles = new Map<string, string>();
|
|
20
|
+
const shownFileListeners = new Set<() => void>();
|
|
21
|
+
|
|
22
|
+
export function showMarkdownFile(workspaceId: string, path: string): void {
|
|
23
|
+
shownFiles.set(workspaceId, path);
|
|
24
|
+
shownFileListeners.forEach((listener) => listener());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function subscribeShownFiles(listener: () => void): () => void {
|
|
28
|
+
shownFileListeners.add(listener);
|
|
29
|
+
return () => shownFileListeners.delete(listener);
|
|
30
|
+
}
|
|
17
31
|
|
|
18
32
|
type Theme = PluginWorkspacePanelProps["theme"];
|
|
19
33
|
type Layout = PluginWorkspacePanelProps["layout"];
|
|
@@ -24,15 +38,14 @@ type DocumentData = { text: string; revision: string };
|
|
|
24
38
|
* file panels get this instead of rendering inside the file tab itself.
|
|
25
39
|
*/
|
|
26
40
|
export function MarkdownPreviewPanel({ workspaceId, theme, layout }: PluginWorkspacePanelProps) {
|
|
27
|
-
const
|
|
41
|
+
const path = useSyncExternalStore(subscribeShownFiles, () => shownFiles.get(workspaceId) ?? null);
|
|
28
42
|
const [picking, setPicking] = useState(false);
|
|
29
43
|
const queryClient = useQueryClient();
|
|
30
44
|
const styles = useMemo(() => panelStyles(theme, layout.compact), [theme, layout.compact]);
|
|
31
45
|
|
|
32
46
|
const open = useCallback(
|
|
33
47
|
(next: string) => {
|
|
34
|
-
|
|
35
|
-
setPath(next);
|
|
48
|
+
showMarkdownFile(workspaceId, next);
|
|
36
49
|
setPicking(false);
|
|
37
50
|
},
|
|
38
51
|
[workspaceId],
|
package/index.client.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import { focusedMarkdownFile, hostStorage } from "./client/active-file";
|
|
2
3
|
import { registerFilePanel } from "./client/file-panel";
|
|
3
|
-
import { MarkdownPreviewPanel } from "./client/workspace-panel";
|
|
4
|
+
import { MarkdownPreviewPanel, showMarkdownFile } from "./client/workspace-panel";
|
|
4
5
|
|
|
5
6
|
export default function contribute(client: PluginClientContext) {
|
|
6
7
|
// Where the host can open Markdown files with a plugin panel, that is the whole integration.
|
|
@@ -20,7 +21,10 @@ export default function contribute(client: PluginClientContext) {
|
|
|
20
21
|
icon: "Sigma",
|
|
21
22
|
keywords: ["markdown", "math", "latex", "ratex", "preview"],
|
|
22
23
|
context: "workspace",
|
|
23
|
-
onSelect({ openPanel }) {
|
|
24
|
+
onSelect({ workspace, openPanel }) {
|
|
25
|
+
// Run from a Markdown file tab, the preview opens on that file.
|
|
26
|
+
const file = focusedMarkdownFile(hostStorage(), workspace);
|
|
27
|
+
if (file) showMarkdownFile(workspace.id, file);
|
|
24
28
|
openPanel("preview");
|
|
25
29
|
},
|
|
26
30
|
});
|