@celestia-island/hikari 0.55.158 → 0.55.160
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 +1 -1
- package/src/components/HkInfiniteTail.tsx +8 -5
- package/src/composables/languageForPath.test.ts +86 -0
- package/src/composables/useHighlight.ts +119 -0
- package/src/composables/useInfinitePages.test.ts +29 -3
- package/src/composables/useInfinitePages.ts +19 -13
- package/src/index.ts +1 -1
- package/src/styles/highlightPalette.contract.test.ts +91 -0
- package/src/styles/index.scss +5 -1
- package/src/styles/theme/highlight.scss +117 -0
- package/src/styles/vendoredSync.test.ts +2 -0
package/package.json
CHANGED
|
@@ -25,11 +25,14 @@ import "./HkInfiniteTail.scss";
|
|
|
25
25
|
* - `"idle"` — more exists, nothing in flight. With `auto` (default)
|
|
26
26
|
* the invisible sentinel watches the viewport and emits `loadMore`
|
|
27
27
|
* as it approaches; with `auto={false}` the explicit load-more
|
|
28
|
-
* button (HkLoadMore) renders instead. NOTE:
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* button (HkLoadMore) renders instead. NOTE: a failed page
|
|
29
|
+
* terminalizes the machine's walk — the tail keeps emitting
|
|
30
|
+
* `loadMore` as the sentinel re-enters view, but each emission is a
|
|
31
|
+
* no-op until the host calls `reload()` (the retry verb; the
|
|
32
|
+
* delivered rows stay on screen under the host's stale banner).
|
|
33
|
+
* Auto-retrying from the tail is deliberately not supported: the
|
|
34
|
+
* sentinel would re-fire on a fixed geometry with no backoff, one
|
|
35
|
+
* RPC per round trip, which locks a shared rate budget.
|
|
33
36
|
*
|
|
34
37
|
* The sentinel observes the BROWSER viewport (IntersectionObserver,
|
|
35
38
|
* `rootMargin` lookahead), so it works inside any nested scroll
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { languageForPath } from "./useHighlight";
|
|
4
|
+
import { LANGUAGE_LOADERS } from "./highlightLanguages";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `languageForPath` is the ONE mapping from a file path to a highlight.js
|
|
8
|
+
* language — shared so a diff pane, a file browser and a markdown fence
|
|
9
|
+
* all derive the same language from the same path. Its two contracts:
|
|
10
|
+
* every non-empty answer must be a language the lazy loader can actually
|
|
11
|
+
* load (otherwise the promise is a lie), and unknown paths must answer ""
|
|
12
|
+
* (callers treat "" as plaintext/auto-detect, never as an error).
|
|
13
|
+
*/
|
|
14
|
+
describe("languageForPath", () => {
|
|
15
|
+
it("maps the common extensions onto canonical ids", () => {
|
|
16
|
+
expect(languageForPath("crates/scheduler/src/main.rs")).toBe("rust");
|
|
17
|
+
expect(languageForPath("src/index.ts")).toBe("typescript");
|
|
18
|
+
expect(languageForPath("App.tsx")).toBe("typescript");
|
|
19
|
+
expect(languageForPath("script.js")).toBe("javascript");
|
|
20
|
+
expect(languageForPath("world.toml")).toBe("toml");
|
|
21
|
+
expect(languageForPath("Cargo.toml")).toBe("toml");
|
|
22
|
+
expect(languageForPath("config/world.yml")).toBe("yaml");
|
|
23
|
+
expect(languageForPath("run.sh")).toBe("bash");
|
|
24
|
+
expect(languageForPath("notes.md")).toBe("markdown");
|
|
25
|
+
expect(languageForPath("query.sql")).toBe("sql");
|
|
26
|
+
expect(languageForPath("theme.scss")).toBe("scss");
|
|
27
|
+
expect(languageForPath("index.html")).toBe("xml");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("is case-insensitive on name and extension", () => {
|
|
31
|
+
expect(languageForPath("src/Main.RS")).toBe("rust");
|
|
32
|
+
expect(languageForPath("Dockerfile")).toBe("dockerfile");
|
|
33
|
+
expect(languageForPath("Makefile")).toBe("makefile");
|
|
34
|
+
expect(languageForPath("CMakeLists.txt")).toBe("cmake");
|
|
35
|
+
expect(languageForPath("Jenkinsfile")).toBe("groovy");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("keeps dotted stems like Dockerfile.dev on their dialect", () => {
|
|
39
|
+
expect(languageForPath("deploy/Dockerfile.dev")).toBe("dockerfile");
|
|
40
|
+
expect(languageForPath("dockerfile.prod")).toBe("dockerfile");
|
|
41
|
+
expect(languageForPath("Makefile.am")).toBe("makefile");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("answers \"\" for unknown or extensionless paths, not a guess", () => {
|
|
45
|
+
expect(languageForPath("bin/run")).toBe("");
|
|
46
|
+
expect(languageForPath("README")).toBe("");
|
|
47
|
+
expect(languageForPath("data.xyzzy")).toBe("");
|
|
48
|
+
expect(languageForPath("")).toBe("");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("every mapped id is a language the loader actually has", () => {
|
|
52
|
+
const loaders = new Set(Object.keys(LANGUAGE_LOADERS));
|
|
53
|
+
expect(loaders.size).toBeGreaterThan(50); // positive control
|
|
54
|
+
const probes = [
|
|
55
|
+
"a.rs", "a.ts", "a.tsx", "a.js", "a.json", "a.py", "a.rb", "a.go",
|
|
56
|
+
"a.java", "a.kt", "a.scala", "a.groovy", "a.c", "a.h", "a.cpp",
|
|
57
|
+
"a.hpp", "a.cs", "a.fs", "a.php", "a.swift", "a.m", "a.mm",
|
|
58
|
+
"a.dart", "a.ex", "a.erl", "a.hs", "a.elm", "a.lua", "a.pl",
|
|
59
|
+
"a.r", "a.jl", "a.nim", "a.cr", "a.ml", "a.vb", "a.pas", "a.bas",
|
|
60
|
+
"a.f90", "a.coffee", "a.ls", "a.clj", "a.scm", "a.el", "a.xml",
|
|
61
|
+
"a.html", "a.vue", "a.hbs", "a.twig", "a.haml", "a.erb", "a.dust",
|
|
62
|
+
"a.css", "a.scss", "a.less", "a.yml", "a.yaml", "a.toml", "a.ini",
|
|
63
|
+
"a.cfg", "a.properties", "a.env", "a.md", "a.graphql", "a.gql",
|
|
64
|
+
"a.http", "a.sh", "a.zsh", "a.ps1", "a.bat", "a.cmd", "a.mk",
|
|
65
|
+
"a.cmake", "a.awk", "a.tcl", "a.vim", "a.sql", "a.psql", "a.proto",
|
|
66
|
+
"a.thrift", "a.glsl", "a.vert", "a.frag", "a.wasm", "a.asm",
|
|
67
|
+
"a.v", "a.sv", "a.vhd", "a.vhdl", "a.tex", "a.sty", "a.diff",
|
|
68
|
+
"a.patch", "a.txt", "a.log", "Dockerfile", "Makefile", "Jenkinsfile",
|
|
69
|
+
"CMakeLists.txt", "Cargo.lock", ".gitignore", ".env", ".bashrc",
|
|
70
|
+
];
|
|
71
|
+
const mapped = probes
|
|
72
|
+
.map((p) => languageForPath(p))
|
|
73
|
+
.filter((id): id is string => id !== "");
|
|
74
|
+
// Each probe resolves to SOMETHING (the table is broad)…
|
|
75
|
+
expect(mapped.length).toBe(probes.length);
|
|
76
|
+
// …and every answer is loadable — no lying promises.
|
|
77
|
+
for (const id of mapped) {
|
|
78
|
+
expect(loaders, `languageForPath answered "${id}", which no loader provides`).toContain(id);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("does not fall off the path on dots in directories", () => {
|
|
83
|
+
expect(languageForPath("my.dir/stuff/main.rs")).toBe("rust");
|
|
84
|
+
expect(languageForPath("v1.2/config.yaml")).toBe("yaml");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -84,6 +84,125 @@ function ensureAutoLangs(): void {
|
|
|
84
84
|
for (const l of AUTO_LANGS) loadLanguage(l);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Whole filenames (lowercased) that carry no useful extension, or whose
|
|
88
|
+
* dialect is decided by the name: `Dockerfile.dev`, `CMakeLists.txt`. */
|
|
89
|
+
const PATH_FILENAME_LANG: Record<string, string> = {
|
|
90
|
+
dockerfile: "dockerfile",
|
|
91
|
+
makefile: "makefile",
|
|
92
|
+
gnumakefile: "makefile",
|
|
93
|
+
"cmakelists.txt": "cmake",
|
|
94
|
+
jenkinsfile: "groovy",
|
|
95
|
+
nginx: "nginx",
|
|
96
|
+
"cargo.lock": "toml",
|
|
97
|
+
".env": "ini",
|
|
98
|
+
".gitignore": "ini",
|
|
99
|
+
".gitattributes": "ini",
|
|
100
|
+
".gitmodules": "ini",
|
|
101
|
+
".editorconfig": "ini",
|
|
102
|
+
".bashrc": "bash",
|
|
103
|
+
".zshrc": "bash",
|
|
104
|
+
".profile": "bash",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Extension (lowercased) → canonical highlight.js language id. Only ids
|
|
108
|
+
* that exist in LANGUAGE_LOADERS: anything else would render plaintext
|
|
109
|
+
* anyway, just with a misleading promise attached. */
|
|
110
|
+
const EXT_TABLE: Record<string, string> = {
|
|
111
|
+
rs: "rust",
|
|
112
|
+
ts: "typescript", tsx: "typescript", mts: "typescript", cts: "typescript",
|
|
113
|
+
js: "javascript", jsx: "javascript", mjs: "javascript", cjs: "javascript",
|
|
114
|
+
json: "json", jsonc: "json",
|
|
115
|
+
py: "python", pyi: "python", pyw: "python",
|
|
116
|
+
rb: "ruby",
|
|
117
|
+
go: "go",
|
|
118
|
+
java: "java",
|
|
119
|
+
kt: "kotlin", kts: "kotlin",
|
|
120
|
+
scala: "scala", sc: "scala",
|
|
121
|
+
groovy: "groovy", gradle: "groovy",
|
|
122
|
+
c: "c", h: "c",
|
|
123
|
+
cpp: "cpp", cc: "cpp", cxx: "cpp", hpp: "cpp", hh: "cpp", hxx: "cpp",
|
|
124
|
+
cs: "csharp",
|
|
125
|
+
fs: "fsharp", fsx: "fsharp",
|
|
126
|
+
php: "php",
|
|
127
|
+
swift: "swift",
|
|
128
|
+
m: "objectivec", mm: "objectivec",
|
|
129
|
+
dart: "dart",
|
|
130
|
+
ex: "elixir", exs: "elixir",
|
|
131
|
+
erl: "erlang", hrl: "erlang",
|
|
132
|
+
hs: "haskell", lhs: "haskell",
|
|
133
|
+
elm: "elm",
|
|
134
|
+
lua: "lua",
|
|
135
|
+
pl: "perl", pm: "perl",
|
|
136
|
+
r: "r",
|
|
137
|
+
jl: "julia",
|
|
138
|
+
nim: "nim",
|
|
139
|
+
cr: "crystal",
|
|
140
|
+
ml: "ocaml", mli: "ocaml", mll: "ocaml", mly: "ocaml",
|
|
141
|
+
vb: "vbnet",
|
|
142
|
+
pas: "delphi", dpr: "delphi",
|
|
143
|
+
bas: "basic",
|
|
144
|
+
f: "fortran", f90: "fortran", f95: "fortran", f03: "fortran", for: "fortran",
|
|
145
|
+
coffee: "coffeescript",
|
|
146
|
+
ls: "livescript",
|
|
147
|
+
clj: "clojure", cljs: "clojure", cljc: "clojure", edn: "clojure",
|
|
148
|
+
scm: "scheme", ss: "scheme", el: "lisp", lisp: "lisp",
|
|
149
|
+
xml: "xml", html: "xml", htm: "xml", svg: "xml", vue: "xml", xhtml: "xml",
|
|
150
|
+
cshtml: "xml", jsp: "xml", erb: "erb", ejs: "xml",
|
|
151
|
+
hbs: "handlebars", handlebars: "handlebars",
|
|
152
|
+
twig: "twig",
|
|
153
|
+
haml: "haml",
|
|
154
|
+
dust: "dust",
|
|
155
|
+
css: "css", scss: "scss", less: "less",
|
|
156
|
+
yml: "yaml", yaml: "yaml",
|
|
157
|
+
toml: "toml",
|
|
158
|
+
ini: "ini", cfg: "ini", conf: "ini", properties: "properties", env: "ini",
|
|
159
|
+
md: "markdown", markdown: "markdown",
|
|
160
|
+
graphql: "graphql", gql: "graphql",
|
|
161
|
+
http: "http",
|
|
162
|
+
sh: "bash", bash: "bash", zsh: "bash", shell: "bash", fish: "bash",
|
|
163
|
+
ps1: "powershell", psm1: "powershell",
|
|
164
|
+
bat: "dos", cmd: "dos",
|
|
165
|
+
mk: "makefile",
|
|
166
|
+
cmake: "cmake",
|
|
167
|
+
dockerfile: "dockerfile",
|
|
168
|
+
nginx: "nginx",
|
|
169
|
+
awk: "awk",
|
|
170
|
+
tcl: "tcl",
|
|
171
|
+
vim: "vim",
|
|
172
|
+
sql: "sql", psql: "pgsql",
|
|
173
|
+
proto: "protobuf",
|
|
174
|
+
thrift: "thrift",
|
|
175
|
+
glsl: "glsl", vert: "glsl", frag: "glsl", geom: "glsl",
|
|
176
|
+
wasm: "wasm",
|
|
177
|
+
asm: "x86asm",
|
|
178
|
+
v: "verilog", sv: "verilog",
|
|
179
|
+
vhd: "vhdl", vhdl: "vhdl",
|
|
180
|
+
tex: "latex", sty: "latex",
|
|
181
|
+
diff: "diff", patch: "diff",
|
|
182
|
+
txt: "plaintext", text: "plaintext", log: "plaintext",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A file path → the canonical highlight.js language id for it, or "" when
|
|
187
|
+
* nothing sensible is known (callers pass "" as "no language" and the
|
|
188
|
+
* block renders plaintext or auto-detects). One mapping for every code
|
|
189
|
+
* surface — diff panes, file browsers, markdown fences — so the same
|
|
190
|
+
* path always highlights the same way; aliases are resolved by
|
|
191
|
+
* `normalize()` at highlight time, so the table lists canonical ids.
|
|
192
|
+
*/
|
|
193
|
+
export function languageForPath(path: string): string {
|
|
194
|
+
const name = path.split("/").pop()?.trim().toLowerCase() ?? "";
|
|
195
|
+
if (!name) return "";
|
|
196
|
+
// Extensionless build scripts: `Dockerfile`, `Jenkinsfile`, `Makefile`.
|
|
197
|
+
const bare = PATH_FILENAME_LANG[name];
|
|
198
|
+
if (bare) return bare;
|
|
199
|
+
// `Dockerfile.dev` / `dockerfile.prod` — the dialect IS the stem.
|
|
200
|
+
if (name.startsWith("dockerfile")) return "dockerfile";
|
|
201
|
+
if (name.startsWith("makefile")) return "makefile";
|
|
202
|
+
const ext = name.includes(".") ? name.split(".").pop()! : "";
|
|
203
|
+
return EXT_TABLE[ext] ?? "";
|
|
204
|
+
}
|
|
205
|
+
|
|
87
206
|
/**
|
|
88
207
|
* Highlight `code` for the given language (or auto-detect when omitted).
|
|
89
208
|
* Returns highlight.js HTML when the grammar is ready, otherwise an
|
|
@@ -114,7 +114,7 @@ describe("useInfinitePages", () => {
|
|
|
114
114
|
expect(handle.exhausted.value).toBe(true);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
it("a failed later page keeps the delivered rows and
|
|
117
|
+
it("a failed later page keeps the delivered rows and terminalizes the walk", async () => {
|
|
118
118
|
const fetcher = vi
|
|
119
119
|
.fn(async (): Promise<HkPageResultLike<Row>> => page([], 0, false))
|
|
120
120
|
.mockImplementationOnce(async () => page([{ id: "a" }], 3, true))
|
|
@@ -127,10 +127,36 @@ describe("useInfinitePages", () => {
|
|
|
127
127
|
await handle.loadMore();
|
|
128
128
|
expect(handle.error.value).toBe("boom");
|
|
129
129
|
expect(handle.items.value.map((r) => r.id)).toEqual(["a"]);
|
|
130
|
-
//
|
|
130
|
+
// A tail failure does NOT auto-retry: the sentinel's idle re-check
|
|
131
|
+
// would re-fire loadMore on a fixed geometry with no backoff — one
|
|
132
|
+
// RPC per round trip forever, locking any shared rate budget. The
|
|
133
|
+
// failure is terminal until `reload`.
|
|
131
134
|
await handle.loadMore();
|
|
135
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
136
|
+
expect(handle.items.value.map((r) => r.id)).toEqual(["a"]);
|
|
137
|
+
// `reload` is the retry verb: it clears the failure and refetches
|
|
138
|
+
// from page one.
|
|
139
|
+
await handle.reload();
|
|
132
140
|
expect(handle.error.value).toBeNull();
|
|
133
|
-
expect(handle.items.value.map((r) => r.id)).toEqual(["
|
|
141
|
+
expect(handle.items.value.map((r) => r.id)).toEqual(["b", "c"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("a tail failure cannot storm: repeated loadMore stays a no-op", async () => {
|
|
145
|
+
const fetcher = vi
|
|
146
|
+
.fn(async (): Promise<HkPageResultLike<Row>> => page([], 0, false))
|
|
147
|
+
.mockImplementationOnce(async () => page([{ id: "a" }], 5, true))
|
|
148
|
+
.mockImplementation(async () => {
|
|
149
|
+
throw new Error("tail down");
|
|
150
|
+
});
|
|
151
|
+
const { handle } = withHandle(() => useInfinitePages<Row>({ fetcher, pageSize: 1 }));
|
|
152
|
+
await vi.waitFor(() => expect(handle.loading.value).toBe(false));
|
|
153
|
+
await handle.loadMore();
|
|
154
|
+
expect(handle.error.value).toBe("tail down");
|
|
155
|
+
// Ten idle-entry re-checks (the tail sentinel's emission pattern)
|
|
156
|
+
// spend ZERO further RPCs.
|
|
157
|
+
for (let i = 0; i < 10; i++) await handle.loadMore();
|
|
158
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
159
|
+
expect(handle.items.value.map((r) => r.id)).toEqual(["a"]);
|
|
134
160
|
});
|
|
135
161
|
|
|
136
162
|
it("a param change triggers a debounced reload from page one", async () => {
|
|
@@ -83,9 +83,9 @@ export interface InfinitePagesHandle<T> {
|
|
|
83
83
|
error: Ref<string | null>;
|
|
84
84
|
/** One-based index of the next page to fetch. */
|
|
85
85
|
nextPage: Ref<number>;
|
|
86
|
-
/** Fetch the next page (guarded: no-op while busy or
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
/** Fetch the next page (guarded: no-op while busy, exhausted, or
|
|
87
|
+
* after ANY failure — `reload` is the retry verb for both the
|
|
88
|
+
* first-page and the tail-page error dialects). */
|
|
89
89
|
loadMore: () => Promise<void>;
|
|
90
90
|
/** Reset to page one and fetch (also the error-clearing retry). */
|
|
91
91
|
reload: () => Promise<void>;
|
|
@@ -101,7 +101,11 @@ export interface InfinitePagesHandle<T> {
|
|
|
101
101
|
*
|
|
102
102
|
* Error dialect: a failed page keeps everything already delivered (the
|
|
103
103
|
* consumer renders it as a stale banner — one failed fetch must never
|
|
104
|
-
* hide rows the user was reading)
|
|
104
|
+
* hide rows the user was reading) and terminalizes the walk; `reload`
|
|
105
|
+
* is the retry verb for every failure. Auto-retrying a tail failure is
|
|
106
|
+
* deliberately NOT done: the tail's sentinel would re-fire on a fixed
|
|
107
|
+
* geometry with no backoff, turning one flaky page into an RPC storm
|
|
108
|
+
* that locks a shared rate budget.
|
|
105
109
|
*/
|
|
106
110
|
export function useInfinitePages<T, P extends Record<string, unknown> = Record<string, unknown>>(
|
|
107
111
|
options: InfinitePagesOptions<T, P>,
|
|
@@ -123,9 +127,6 @@ export function useInfinitePages<T, P extends Record<string, unknown> = Record<s
|
|
|
123
127
|
let seq = 0;
|
|
124
128
|
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
125
129
|
let disposed = false;
|
|
126
|
-
/** Which phase the current `error` came from — a tail-page failure
|
|
127
|
-
* retried by `loadMore` in place, a first-page failure by `reload`. */
|
|
128
|
-
let errorPhase: "load" | "more" = "load";
|
|
129
130
|
|
|
130
131
|
function normalize(result: HkPageResultLike<T>): HkPageResult<T> {
|
|
131
132
|
const loose = result as {
|
|
@@ -196,14 +197,20 @@ export function useInfinitePages<T, P extends Record<string, unknown> = Record<s
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
async function loadMore(): Promise<void> {
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
200
|
+
// ANY failure terminalizes the walk until `reload()` — including a
|
|
201
|
+
// tail-page failure. The in-place retry this used to allow read
|
|
202
|
+
// fine in isolation but stormed in the field: a failed tail leaves
|
|
203
|
+
// the state machine "idle", the tail's sentinel stays in view, and
|
|
204
|
+
// its idle-entry re-check fires loadMore again with no geometry
|
|
205
|
+
// change — one RPC per round trip, no backoff, forever. On a shared
|
|
206
|
+
// per-user rate budget that loop locks the bucket and denies every
|
|
207
|
+
// OTHER paged page, so the retry verb is `reload()` (the delivered
|
|
208
|
+
// rows stay on screen under the stale banner; the header refresh is
|
|
209
|
+
// the user's affordance).
|
|
202
210
|
if (loading.value || loadingMore.value || exhausted.value) return;
|
|
203
|
-
if (error.value
|
|
211
|
+
if (error.value) return;
|
|
204
212
|
if (nextPage.value > 1 && items.value.length === 0) return;
|
|
205
213
|
loadingMore.value = true;
|
|
206
|
-
errorPhase = "more";
|
|
207
214
|
const mine = ++seq;
|
|
208
215
|
try {
|
|
209
216
|
await fetchPage(nextPage.value, mine);
|
|
@@ -225,7 +232,6 @@ export function useInfinitePages<T, P extends Record<string, unknown> = Record<s
|
|
|
225
232
|
reloadTimer = null;
|
|
226
233
|
}
|
|
227
234
|
loading.value = true;
|
|
228
|
-
errorPhase = "load";
|
|
229
235
|
error.value = null;
|
|
230
236
|
if (!options.keepDuringReload) {
|
|
231
237
|
// Invalidate the cursor BEFORE the fetch so a straggler from the
|
package/src/index.ts
CHANGED
|
@@ -460,7 +460,7 @@ export type { OriginProbe } from "./utils/connectivity";
|
|
|
460
460
|
export { probeHealthEndpoint } from "./utils/healthProbe";
|
|
461
461
|
export type { HealthProbeBody, HealthProbeResult } from "./utils/healthProbe";
|
|
462
462
|
|
|
463
|
-
export { highlight, useHighlight } from "./composables/useHighlight";
|
|
463
|
+
export { highlight, languageForPath, useHighlight } from "./composables/useHighlight";
|
|
464
464
|
export { LANGUAGE_LOADERS } from "./composables/highlightLanguages";
|
|
465
465
|
export {
|
|
466
466
|
iconByName,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The syntax-palette contract (theme/highlight.scss).
|
|
9
|
+
*
|
|
10
|
+
* `useHighlight()` emits `.hljs-*` spans; this sheet is the ONLY thing
|
|
11
|
+
* that colours them, so the pairs documented in channels.scss must stay
|
|
12
|
+
* mapped to their `--code-*` tokens — if a restyle ever swaps a var() for
|
|
13
|
+
* a bare hex, themed hosts stop flipping light/dark for that class and
|
|
14
|
+
* the sheet silently forks the palette the host owns.
|
|
15
|
+
*
|
|
16
|
+
* Assertions run on the BODY (comment lines stripped): the header and
|
|
17
|
+
* inline comments deliberately NAME the classes they explain, so matching
|
|
18
|
+
* against the raw sheet would hit prose instead of rules.
|
|
19
|
+
*/
|
|
20
|
+
const body = readFileSync(
|
|
21
|
+
resolve(dirname(fileURLToPath(import.meta.url)), "theme/highlight.scss"),
|
|
22
|
+
"utf-8",
|
|
23
|
+
)
|
|
24
|
+
.split("\n")
|
|
25
|
+
.filter((line) => !line.trim().startsWith("//"))
|
|
26
|
+
.join("\n");
|
|
27
|
+
|
|
28
|
+
/** Pull one declaration block by a selector fragment (regex-escaped). */
|
|
29
|
+
function blockFor(selector: string): string {
|
|
30
|
+
const esc = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
const re = new RegExp(
|
|
32
|
+
String.raw`\.hljs-${esc}\b[\s\S]*?\{([^}]*)\}`,
|
|
33
|
+
);
|
|
34
|
+
const m = re.exec(body);
|
|
35
|
+
expect(m, `positive control: the .hljs-${selector} rule exists`).not.toBeNull();
|
|
36
|
+
return m![1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("highlight palette (theme/highlight.scss)", () => {
|
|
40
|
+
it("rides the --code-* family for every colour, One Dark as fallback", () => {
|
|
41
|
+
const colorRules = body.match(/color:\s*[^;]+;/g) ?? [];
|
|
42
|
+
expect(colorRules.length).toBeGreaterThan(5); // positive control
|
|
43
|
+
for (const rule of colorRules) {
|
|
44
|
+
expect(
|
|
45
|
+
rule,
|
|
46
|
+
`every colour must ride the token family: "${rule}"`,
|
|
47
|
+
).toMatch(/color:\s*var\(--code-[a-z]+,\s*#[0-9a-fA-F]{6}\);/);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("keeps the pairs channels.scss documents canonical", () => {
|
|
52
|
+
expect(blockFor("keyword")).toContain("var(--code-keyword, #c678dd)");
|
|
53
|
+
expect(blockFor("selector-tag")).toContain("var(--code-keyword, #c678dd)");
|
|
54
|
+
expect(blockFor("built_in")).toContain("var(--code-builtin, #c678dd)");
|
|
55
|
+
expect(blockFor("string")).toContain("var(--code-string, #98c379)");
|
|
56
|
+
expect(blockFor("number")).toContain("var(--code-number, #d19a66)");
|
|
57
|
+
expect(blockFor("literal")).toContain("var(--code-number, #d19a66)");
|
|
58
|
+
expect(blockFor("attr")).toContain("var(--code-number, #d19a66)");
|
|
59
|
+
expect(blockFor("title.function_")).toContain("var(--code-function, #61afef)");
|
|
60
|
+
expect(blockFor("type")).toContain("var(--code-type, #e5c07b)");
|
|
61
|
+
expect(blockFor("title.class_")).toContain("var(--code-type, #e5c07b)");
|
|
62
|
+
expect(blockFor("variable")).toContain("var(--code-variable, #e06c75)");
|
|
63
|
+
expect(blockFor("template-variable")).toContain("var(--code-variable, #e06c75)");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders comments grey AND italic, as documented", () => {
|
|
67
|
+
const b = blockFor("comment");
|
|
68
|
+
expect(b).toContain("var(--code-comment, #5c6370)");
|
|
69
|
+
expect(b).toMatch(/font-style:\s*italic/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not paint backgrounds or the .hljs base — surfaces own those", () => {
|
|
73
|
+
expect(body).not.toMatch(/background[^;{]*:/);
|
|
74
|
+
expect(body).not.toMatch(/^\.hljs\s*\{/m);
|
|
75
|
+
// The diff classes belong to the diff surfaces, not the palette.
|
|
76
|
+
expect(body).not.toContain(".hljs-addition");
|
|
77
|
+
expect(body).not.toContain(".hljs-deletion");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("is actually composed into the styles entry — a sheet nobody @uses is dead", () => {
|
|
81
|
+
const entry = readFileSync(
|
|
82
|
+
resolve(dirname(fileURLToPath(import.meta.url)), "index.scss"),
|
|
83
|
+
"utf-8",
|
|
84
|
+
);
|
|
85
|
+
expect(
|
|
86
|
+
entry.match(/@use\s+"\.\/theme\/[^"]+\.scss";/g) ?? [],
|
|
87
|
+
"positive control: the entry composes theme sheets",
|
|
88
|
+
).toContain("@use \"./theme/channels.scss\";");
|
|
89
|
+
expect(entry).toContain('@use "./theme/highlight.scss";');
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/styles/index.scss
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
// 3. theme/scrollbar.scss — the shared overlay-scrollbar chrome
|
|
13
13
|
// (.hk-scrollbar-track/thumb) that attachOverlayScrollbars() attaches
|
|
14
14
|
// to every scroll region (vendored from packages/theme/styles).
|
|
15
|
-
// 4.
|
|
15
|
+
// 4. theme/highlight.scss — the syntax palette: colours the .hljs-*
|
|
16
|
+
// spans useHighlight() emits, riding the --code-* family the host
|
|
17
|
+
// theme owns (vendored from packages/theme/styles).
|
|
18
|
+
// 5. admin-tokens.scss — the plana-ui shared surface: layout tokens,
|
|
16
19
|
// brand-color aliases and the shared admin component classes.
|
|
17
20
|
//
|
|
18
21
|
// initTheme() (runtime) writes only deltas against this base into a
|
|
@@ -28,4 +31,5 @@
|
|
|
28
31
|
@use "./theme/channels.scss";
|
|
29
32
|
@use "./theme/scale.scss";
|
|
30
33
|
@use "./theme/scrollbar.scss";
|
|
34
|
+
@use "./theme/highlight.scss";
|
|
31
35
|
@use "./admin-tokens.scss";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Vendored from packages/theme/styles/_highlight.scss so npm registry
|
|
2
|
+
// installs can resolve it — the exports map only exposes ./styles/*.
|
|
3
|
+
// This sheet is what COLOURS the `.hljs-*` spans that useHighlight() and
|
|
4
|
+
// any other highlight.js driver emit; without it the grammars load and
|
|
5
|
+
// the code still renders monochrome (infra without a palette).
|
|
6
|
+
// Edit the upstream file, then re-copy; do not hand-edit here.
|
|
7
|
+
|
|
8
|
+
// _highlight.scss — the syntax palette for highlight.js output.
|
|
9
|
+
//
|
|
10
|
+
// `useHighlight()` (and anything else driving highlight.js) emits
|
|
11
|
+
// `.hljs-*` spans; this sheet is what actually COLOURS them. Without it
|
|
12
|
+
// the grammars load, the spans land in the DOM, and the code stays
|
|
13
|
+
// monochrome — infra without a palette.
|
|
14
|
+
//
|
|
15
|
+
// Every colour rides the `--code-*` family documented in channels.scss
|
|
16
|
+
// (the family the host theme owns; extend it only by adding a token with
|
|
17
|
+
// a fallback equal to the current visual). The fallbacks here are the
|
|
18
|
+
// canonical One Dark values, so a standalone/themeless consumer renders
|
|
19
|
+
// like One Dark and a themed host (which defines `--code-*` in both
|
|
20
|
+
// modes) flips light/dark automatically — no `.hljs-*` rule anywhere in
|
|
21
|
+
// this sheet hardcodes a palette colour outside a var() fallback.
|
|
22
|
+
//
|
|
23
|
+
// The explicit pairs from the channels.scss doc block are canonical:
|
|
24
|
+
// keyword ← .hljs-keyword / .hljs-selector-tag
|
|
25
|
+
// builtin ← .hljs-built_in
|
|
26
|
+
// string ← .hljs-string
|
|
27
|
+
// number ← .hljs-number / .hljs-literal / .hljs-attr
|
|
28
|
+
// comment ← .hljs-comment / .hljs-doctag (grey, italic)
|
|
29
|
+
// function ← .hljs-title.function_
|
|
30
|
+
// type ← .hljs-type / class titles
|
|
31
|
+
// variable ← .hljs-variable / .hljs-template-variable
|
|
32
|
+
// The remaining highlight.js classes are bucketed to the nearest of the
|
|
33
|
+
// same eight by judgement (the canonical pairs above are exact); the
|
|
34
|
+
// goal is that every span lands in the family, not that each bucket
|
|
35
|
+
// matches One Dark's own file class-for-class.
|
|
36
|
+
//
|
|
37
|
+
// Deliberately NOT set: `.hljs` base colour/background and the
|
|
38
|
+
// `.hljs-addition` / `.hljs-deletion` diff classes — containers (diff
|
|
39
|
+
// panes, markdown fences) own their surfaces, and a palette layer that
|
|
40
|
+
// painted backgrounds would fight them.
|
|
41
|
+
|
|
42
|
+
// keyword — purple
|
|
43
|
+
.hljs-keyword,
|
|
44
|
+
.hljs-selector-tag,
|
|
45
|
+
.hljs-formula {
|
|
46
|
+
color: var(--code-keyword, #c678dd);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// builtin — purple in dark, blue in light
|
|
50
|
+
.hljs-built_in,
|
|
51
|
+
.hljs-selector-id,
|
|
52
|
+
.hljs-symbol,
|
|
53
|
+
.hljs-bullet,
|
|
54
|
+
.hljs-link,
|
|
55
|
+
.hljs-meta {
|
|
56
|
+
color: var(--code-builtin, #c678dd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// string — green
|
|
60
|
+
.hljs-string,
|
|
61
|
+
.hljs-regexp,
|
|
62
|
+
.hljs-attribute,
|
|
63
|
+
.hljs-meta .hljs-string {
|
|
64
|
+
color: var(--code-string, #98c379);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// number — orange in dark, blue in light
|
|
68
|
+
.hljs-number,
|
|
69
|
+
.hljs-literal,
|
|
70
|
+
.hljs-attr,
|
|
71
|
+
.hljs-property,
|
|
72
|
+
.hljs-selector-class,
|
|
73
|
+
.hljs-selector-attr,
|
|
74
|
+
.hljs-selector-pseudo {
|
|
75
|
+
color: var(--code-number, #d19a66);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// comment — grey, italic
|
|
79
|
+
.hljs-comment,
|
|
80
|
+
.hljs-quote,
|
|
81
|
+
.hljs-doctag {
|
|
82
|
+
color: var(--code-comment, #5c6370);
|
|
83
|
+
font-style: italic;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// function — blue
|
|
87
|
+
.hljs-title,
|
|
88
|
+
.hljs-title.function_,
|
|
89
|
+
.hljs-title.function_.invoke__,
|
|
90
|
+
.hljs-section {
|
|
91
|
+
color: var(--code-function, #61afef);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// type — yellow in dark, green in light
|
|
95
|
+
.hljs-type,
|
|
96
|
+
.hljs-title.class_,
|
|
97
|
+
.hljs-class .hljs-title {
|
|
98
|
+
color: var(--code-type, #e5c07b);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// variable — red in dark, orange in light
|
|
102
|
+
.hljs-variable,
|
|
103
|
+
.hljs-template-variable,
|
|
104
|
+
.hljs-subst,
|
|
105
|
+
.hljs-name,
|
|
106
|
+
.hljs-tag {
|
|
107
|
+
color: var(--code-variable, #e06c75);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Non-colour emphasis classes: keep the shapes highlight.js intends.
|
|
111
|
+
.hljs-emphasis {
|
|
112
|
+
font-style: italic;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.hljs-strong {
|
|
116
|
+
font-weight: bold;
|
|
117
|
+
}
|
|
@@ -52,6 +52,8 @@ const VENDORED_FROM_VUE: Array<[string, string]> = [
|
|
|
52
52
|
// (packages/theme/styles) and is vendored here so npm consumers can
|
|
53
53
|
// resolve it — the exports map only exposes ./styles/*.
|
|
54
54
|
["theme/scrollbar.scss", "../../theme/styles/_scrollbar.scss"],
|
|
55
|
+
// The syntax palette for .hljs-* spans lives there for the same reason.
|
|
56
|
+
["theme/highlight.scss", "../../theme/styles/_highlight.scss"],
|
|
55
57
|
];
|
|
56
58
|
|
|
57
59
|
|