@cueloop/client 0.1.0-alpha.0 → 0.1.0-alpha.11

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/src/syntax.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Code-block syntax highlighting via Shiki (#67): TextMate-grammar tokens
3
+ * colored by a theme built from cueloop's tokens. The highlighter loads
4
+ * lazily on the first code block; unknown languages render unstyled.
5
+ */
6
+
7
+ import { DARK as T } from "./theme";
8
+
9
+ export interface CodeToken {
10
+ content: string;
11
+ color?: string;
12
+ }
13
+
14
+ type Highlighter = {
15
+ codeToTokensBase(code: string, opts: { lang: string; theme: string }): { content: string; color?: string }[][];
16
+ getLoadedLanguages(): string[];
17
+ };
18
+
19
+ const LANGS = ["typescript", "tsx", "javascript", "jsx", "json", "bash", "python", "rust", "go", "yaml", "diff", "markdown", "toml", "sql", "html", "css"];
20
+
21
+ /** Markdown fence info → Shiki language id. */
22
+ export function filetypeFor(lang?: string): string {
23
+ const l = (lang ?? "").toLowerCase();
24
+ const map: Record<string, string> = {
25
+ ts: "typescript",
26
+ js: "javascript",
27
+ py: "python",
28
+ rb: "ruby",
29
+ rs: "rust",
30
+ sh: "bash",
31
+ shell: "bash",
32
+ zsh: "bash",
33
+ yml: "yaml",
34
+ };
35
+ return map[l] ?? l;
36
+ }
37
+
38
+ function cueloopTheme() {
39
+ const scope = (scopes: string, foreground: string, fontStyle?: string) => ({
40
+ scope: scopes.split(", "),
41
+ settings: fontStyle ? { foreground, fontStyle } : { foreground },
42
+ });
43
+ return {
44
+ name: "cueloop",
45
+ type: "dark" as const,
46
+ colors: { "editor.background": T.elevated, "editor.foreground": T.textMuted },
47
+ settings: [
48
+ { settings: { foreground: T.textMuted } },
49
+ scope("comment, punctuation.definition.comment", T.textDim, "italic"),
50
+ scope("string, string.quoted, punctuation.definition.string", T.green),
51
+ scope("constant.numeric, constant.language, constant.character", T.blue),
52
+ scope("keyword, storage, storage.type, storage.modifier, keyword.control", T.accent),
53
+ scope("entity.name.function, support.function, meta.function-call", T.text),
54
+ scope("entity.name.type, entity.name.class, support.type, support.class", T.blue),
55
+ scope("variable, variable.other, variable.parameter", T.textMuted),
56
+ scope("entity.name.tag", T.accent),
57
+ scope("entity.other.attribute-name", T.blue),
58
+ scope("punctuation, meta.brace", T.textDim),
59
+ scope("keyword.operator", T.textDim),
60
+ ],
61
+ };
62
+ }
63
+
64
+ let highlighterPromise: Promise<Highlighter | null> | null = null;
65
+
66
+ function getHighlighter(): Promise<Highlighter | null> {
67
+ highlighterPromise ??= import("shiki")
68
+ .then((shiki) => shiki.createHighlighter({ themes: [cueloopTheme()], langs: LANGS }) as Promise<Highlighter>)
69
+ .catch(() => null); // highlighting is an enhancement, never a blocker
70
+ return highlighterPromise;
71
+ }
72
+
73
+ /**
74
+ * Tokenize code into styled lines. Token text concatenates back to the exact
75
+ * source line, so verbatim rendering (indentation, no wrap) is preserved.
76
+ * Returns null when the language is unknown or the highlighter is unavailable.
77
+ */
78
+ export async function highlightCode(content: string, lang?: string): Promise<CodeToken[][] | null> {
79
+ const language = filetypeFor(lang);
80
+ if (!language) return null;
81
+ const highlighter = await getHighlighter();
82
+ if (!highlighter || !highlighter.getLoadedLanguages().includes(language)) return null;
83
+ try {
84
+ return highlighter.codeToTokensBase(content, { lang: language, theme: "cueloop" });
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
package/src/view.test.ts CHANGED
@@ -2,17 +2,15 @@ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  blockRuns,
4
4
  buildDisplay,
5
- cutBlock,
6
5
  displayText,
7
6
  marksByDisplay,
7
+ nextWorkBlock,
8
8
  overlayMarks,
9
- restoreBlock,
10
- restoreLine,
11
9
  spanKey,
12
10
  startSpan,
13
11
  wrapRuns,
14
12
  } from "./view";
15
- import { parseBlocks, type Annotation } from "@cueloop/schema";
13
+ import { cutBlock, parseBlocks, restoreBlock, restoreLine, type Annotation } from "@cueloop/schema";
16
14
 
17
15
  const BASE = `# Plan
18
16
 
@@ -150,8 +148,10 @@ describe("cut / restore round-trip", () => {
150
148
  expect(cut).not.toContain("second item");
151
149
  const display = buildDisplay(BASE, cut);
152
150
  const delIdx = display.findIndex((d) => d.type === "del");
153
- const line = restoreLine(display, delIdx, cut.split("\n").length);
154
- const restored = restoreBlock(cut, "- second item", line);
151
+ const line = restoreLine(nextWorkBlock(display, delIdx), cut.split("\n").length);
152
+ // undefined = the working copy round-tripped back to the submitted revision
153
+ const restored = restoreBlock(BASE, cut, li, line);
154
+ expect(restored).toBeUndefined();
155
155
  expect(buildDisplay(BASE, restored).every((d) => d.type === "same")).toBe(true);
156
156
  });
157
157
  });
package/src/view.ts CHANGED
Binary file