@particle-academy/fancy-code 0.7.0 → 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.
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import { createContext, useState, useCallback, useRef, useEffect, useMemo, useSyncExternalStore, useContext } from 'react';
1
+ import { createContext, useMemo, useState, useCallback, useRef, useEffect, useSyncExternalStore, useContext } from 'react';
2
2
  import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
3
+ import { annotateLines, highlightCode, tok, tokenizeHtml, LIGHT_COLORS, DARK_COLORS, languageFromFilename } from '@particle-academy/fancy-file-commons';
4
+ export { languageFromFilename } from '@particle-academy/fancy-file-commons';
3
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
6
 
5
7
  // src/components/CodeEditor/CodeEditor.tsx
@@ -11,6 +13,24 @@ function useCodeEditor() {
11
13
  }
12
14
  return ctx;
13
15
  }
16
+
17
+ // src/components/CodeEditor/diff-gutter.ts
18
+ function gutterDiffMark(annotations, line) {
19
+ if (!annotations) return null;
20
+ const a = annotations.byLine[line];
21
+ if (!a) return null;
22
+ return { type: a.type, deletedAbove: (a.deletedAbove ?? 0) > 0 };
23
+ }
24
+ function hasDeletedAtEnd(annotations) {
25
+ return (annotations?.deletedAtEnd ?? 0) > 0;
26
+ }
27
+ var FALLBACK = { added: "#3fb950", modified: "#4184e4", removed: "#f85149" };
28
+ function diffMarkColor(type, colors) {
29
+ return type === "added" ? colors.diffAdded ?? FALLBACK.added : colors.diffModified ?? FALLBACK.modified;
30
+ }
31
+ function diffRemovedColor(colors) {
32
+ return colors.diffRemoved ?? FALLBACK.removed;
33
+ }
14
34
  function CodeEditorPanel({ className }) {
15
35
  const {
16
36
  _engineReturn: engine,
@@ -18,6 +38,7 @@ function CodeEditorPanel({ className }) {
18
38
  wordWrap,
19
39
  readOnly,
20
40
  placeholder,
41
+ _diffAnnotations: diffAnnotations,
21
42
  _maxHeight,
22
43
  _minHeight
23
44
  } = useCodeEditor();
@@ -38,25 +59,80 @@ function CodeEditorPanel({ className }) {
38
59
  const gutterWidth = lineNumbers ? `${Math.max(String(lineCount).length, 2) * 0.75 + 1.5}em` : "0";
39
60
  const lineNumberElements = useMemo(() => {
40
61
  if (!lineNumbers) return null;
62
+ const removed = diffRemovedColor(themeColors);
41
63
  const lines = [];
42
64
  for (let i = 1; i <= lineCount; i++) {
65
+ const mark = gutterDiffMark(diffAnnotations, i);
43
66
  lines.push(
44
- /* @__PURE__ */ jsx(
67
+ /* @__PURE__ */ jsxs(
45
68
  "div",
46
69
  {
70
+ "data-diff": mark?.type,
71
+ "data-diff-deleted-above": mark?.deletedAbove ? "" : void 0,
47
72
  style: {
73
+ position: "relative",
48
74
  color: i === activeLine ? themeColors.foreground : themeColors.gutterForeground,
49
75
  backgroundColor: i === activeLine ? themeColors.activeLineBackground : void 0
50
76
  },
51
77
  className: "pr-3 text-right text-[13px] leading-[1.5] select-none",
52
- children: i
78
+ children: [
79
+ mark?.type && /* @__PURE__ */ jsx(
80
+ "span",
81
+ {
82
+ "aria-hidden": "true",
83
+ style: {
84
+ position: "absolute",
85
+ left: 0,
86
+ top: 0,
87
+ bottom: 0,
88
+ width: 3,
89
+ borderRadius: 2,
90
+ backgroundColor: diffMarkColor(mark.type, themeColors)
91
+ }
92
+ }
93
+ ),
94
+ mark?.deletedAbove && /* @__PURE__ */ jsx(
95
+ "span",
96
+ {
97
+ "aria-hidden": "true",
98
+ style: {
99
+ position: "absolute",
100
+ left: 0,
101
+ top: -4,
102
+ width: 0,
103
+ height: 0,
104
+ borderLeft: `8px solid ${removed}`,
105
+ borderTop: "4px solid transparent",
106
+ borderBottom: "4px solid transparent"
107
+ }
108
+ }
109
+ ),
110
+ i,
111
+ i === lineCount && hasDeletedAtEnd(diffAnnotations) && /* @__PURE__ */ jsx(
112
+ "span",
113
+ {
114
+ "aria-hidden": "true",
115
+ "data-diff-deleted-at-end": "",
116
+ style: {
117
+ position: "absolute",
118
+ left: 0,
119
+ bottom: -4,
120
+ width: 0,
121
+ height: 0,
122
+ borderLeft: `8px solid ${removed}`,
123
+ borderTop: "4px solid transparent",
124
+ borderBottom: "4px solid transparent"
125
+ }
126
+ }
127
+ )
128
+ ]
53
129
  },
54
130
  i
55
131
  )
56
132
  );
57
133
  }
58
134
  return lines;
59
- }, [lineNumbers, lineCount, activeLine, themeColors]);
135
+ }, [lineNumbers, lineCount, activeLine, themeColors, diffAnnotations]);
60
136
  const containerStyle = {
61
137
  backgroundColor: themeColors.background,
62
138
  color: themeColors.foreground,
@@ -369,89 +445,6 @@ var tokenizeJavaScript = (source) => {
369
445
  return tokens;
370
446
  };
371
447
 
372
- // src/engine/tokenizers/html.ts
373
- var tokenizeHtml = (source) => {
374
- const tokens = [];
375
- const len = source.length;
376
- let i = 0;
377
- while (i < len) {
378
- if (source[i] === "<" && source.slice(i, i + 4) === "<!--") {
379
- const pos2 = i;
380
- i += 4;
381
- while (i < len && source.slice(i, i + 3) !== "-->") i++;
382
- i += 3;
383
- tokens.push({ type: "comment", start: pos2, end: i });
384
- continue;
385
- }
386
- if (source[i] === "<" && source.slice(i, i + 9).toUpperCase() === "<!DOCTYPE") {
387
- const pos2 = i;
388
- while (i < len && source[i] !== ">") i++;
389
- i++;
390
- tokens.push({ type: "keyword", start: pos2, end: i });
391
- continue;
392
- }
393
- if (source[i] === "<") {
394
- tokens.push({ type: "punctuation", start: i, end: i + 1 });
395
- i++;
396
- if (i < len && source[i] === "/") {
397
- tokens.push({ type: "punctuation", start: i, end: i + 1 });
398
- i++;
399
- }
400
- if (i < len && /[a-zA-Z]/.test(source[i])) {
401
- const pos2 = i;
402
- while (i < len && /[a-zA-Z0-9-]/.test(source[i])) i++;
403
- tokens.push({ type: "tag", start: pos2, end: i });
404
- }
405
- while (i < len && source[i] !== ">" && !(source[i] === "/" && source[i + 1] === ">")) {
406
- if (source[i] === " " || source[i] === " " || source[i] === "\n" || source[i] === "\r") {
407
- i++;
408
- continue;
409
- }
410
- if (/[a-zA-Z_@:]/.test(source[i])) {
411
- const pos2 = i;
412
- while (i < len && /[a-zA-Z0-9_\-:.]/.test(source[i])) i++;
413
- tokens.push({ type: "attribute", start: pos2, end: i });
414
- while (i < len && (source[i] === " " || source[i] === " ")) i++;
415
- if (i < len && source[i] === "=") {
416
- tokens.push({ type: "operator", start: i, end: i + 1 });
417
- i++;
418
- while (i < len && (source[i] === " " || source[i] === " ")) i++;
419
- if (i < len && (source[i] === '"' || source[i] === "'")) {
420
- const quote = source[i];
421
- const pos3 = i;
422
- i++;
423
- while (i < len && source[i] !== quote) i++;
424
- i++;
425
- tokens.push({ type: "attributeValue", start: pos3, end: i });
426
- } else if (i < len && /[^\s>]/.test(source[i])) {
427
- const pos3 = i;
428
- while (i < len && /[^\s>]/.test(source[i])) i++;
429
- tokens.push({ type: "attributeValue", start: pos3, end: i });
430
- }
431
- }
432
- continue;
433
- }
434
- i++;
435
- }
436
- if (i < len && source[i] === "/") {
437
- tokens.push({ type: "punctuation", start: i, end: i + 1 });
438
- i++;
439
- }
440
- if (i < len && source[i] === ">") {
441
- tokens.push({ type: "punctuation", start: i, end: i + 1 });
442
- i++;
443
- }
444
- continue;
445
- }
446
- const pos = i;
447
- while (i < len && source[i] !== "<") i++;
448
- if (i > pos) {
449
- tokens.push({ type: "plain", start: pos, end: i });
450
- }
451
- }
452
- return tokens;
453
- };
454
-
455
448
  // src/engine/tokenizers/php.ts
456
449
  var PHP_KEYWORDS = /* @__PURE__ */ new Set([
457
450
  "abstract",
@@ -1035,11 +1028,6 @@ var tokenizeGo = (source) => {
1035
1028
  return tokens;
1036
1029
  };
1037
1030
 
1038
- // src/engine/tokenizer.ts
1039
- function tok(type, s, e) {
1040
- return { type, start: s, end: e };
1041
- }
1042
-
1043
1031
  // src/engine/tokenizers/markdown.ts
1044
1032
  function tokenizeMarkdown(source) {
1045
1033
  const tokens = [];
@@ -1244,106 +1232,17 @@ function getTheme(name) {
1244
1232
  function getRegisteredThemes() {
1245
1233
  return Array.from(themeRegistry.keys());
1246
1234
  }
1247
-
1248
- // src/themes/light.ts
1249
1235
  registerTheme({
1250
1236
  name: "light",
1251
1237
  variant: "light",
1252
- colors: {
1253
- background: "#ffffff",
1254
- foreground: "#1e1e2e",
1255
- gutterBackground: "#f8fafc",
1256
- gutterForeground: "#94a3b8",
1257
- gutterBorder: "#e2e8f0",
1258
- activeLineBackground: "#f8fafc",
1259
- selectionBackground: "#dbeafe",
1260
- cursorColor: "#3b82f6",
1261
- keyword: "#8b5cf6",
1262
- string: "#059669",
1263
- comment: "#94a3b8",
1264
- number: "#d97706",
1265
- operator: "#0891b2",
1266
- function: "#2563eb",
1267
- type: "#d97706",
1268
- tag: "#dc2626",
1269
- attribute: "#d97706",
1270
- attributeValue: "#059669",
1271
- punctuation: "#64748b",
1272
- variable: "#1e1e2e"
1273
- }
1238
+ colors: LIGHT_COLORS
1274
1239
  });
1275
-
1276
- // src/themes/dark.ts
1277
1240
  registerTheme({
1278
1241
  name: "dark",
1279
1242
  variant: "dark",
1280
- colors: {
1281
- background: "#18181b",
1282
- foreground: "#e4e4e7",
1283
- gutterBackground: "#18181b",
1284
- gutterForeground: "#52525b",
1285
- gutterBorder: "#27272a",
1286
- activeLineBackground: "#27272a",
1287
- selectionBackground: "#1e3a5f",
1288
- cursorColor: "#60a5fa",
1289
- keyword: "#c084fc",
1290
- string: "#34d399",
1291
- comment: "#71717a",
1292
- number: "#fbbf24",
1293
- operator: "#22d3ee",
1294
- function: "#60a5fa",
1295
- type: "#fbbf24",
1296
- tag: "#f87171",
1297
- attribute: "#fbbf24",
1298
- attributeValue: "#34d399",
1299
- punctuation: "#a1a1aa",
1300
- variable: "#e4e4e7"
1301
- }
1243
+ colors: DARK_COLORS
1302
1244
  });
1303
1245
 
1304
- // src/engine/highlight.ts
1305
- function escapeHtml(text) {
1306
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1307
- }
1308
- var COLOR_RE = /^(#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla)\([\d\s,.%/-]+\)|[a-zA-Z]+)$/;
1309
- function safeColor(value) {
1310
- return typeof value === "string" && COLOR_RE.test(value.trim()) ? value.trim() : "inherit";
1311
- }
1312
- function highlightCode(source, tokens, colors) {
1313
- if (tokens.length === 0) return escapeHtml(source);
1314
- const colorMap = {
1315
- keyword: colors.keyword,
1316
- string: colors.string,
1317
- comment: colors.comment,
1318
- number: colors.number,
1319
- operator: colors.operator,
1320
- function: colors.function,
1321
- type: colors.type,
1322
- tag: colors.tag,
1323
- attribute: colors.attribute,
1324
- attributeValue: colors.attributeValue,
1325
- punctuation: colors.punctuation,
1326
- variable: colors.variable,
1327
- plain: colors.foreground
1328
- };
1329
- const parts = [];
1330
- let pos = 0;
1331
- for (const token of tokens) {
1332
- if (token.start > pos) {
1333
- parts.push(escapeHtml(source.slice(pos, token.start)));
1334
- }
1335
- const text = escapeHtml(source.slice(token.start, token.end));
1336
- const color = safeColor(colorMap[token.type]);
1337
- const style = token.type === "comment" ? `color:${color};font-style:italic` : `color:${color}`;
1338
- parts.push(`<span style="${style}">${text}</span>`);
1339
- pos = token.end;
1340
- }
1341
- if (pos < source.length) {
1342
- parts.push(escapeHtml(source.slice(pos)));
1343
- }
1344
- return parts.join("");
1345
- }
1346
-
1347
1246
  // src/engine/position.ts
1348
1247
  function lineColumnToOffset(text, line, column = 1) {
1349
1248
  const lines = text.split("\n");
@@ -1615,9 +1514,14 @@ function CodeEditorRoot({
1615
1514
  minHeight,
1616
1515
  maxHeight,
1617
1516
  cursorLine,
1618
- cursorColumn
1517
+ cursorColumn,
1518
+ diffBase
1619
1519
  }) {
1620
1520
  const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);
1521
+ const diffAnnotations = useMemo(
1522
+ () => diffBase == null ? null : annotateLines(diffBase, currentValue),
1523
+ [diffBase, currentValue]
1524
+ );
1621
1525
  const isDark = useDarkMode();
1622
1526
  const resolvedTheme = theme === "auto" ? isDark ? "dark" : "light" : theme;
1623
1527
  const [currentLanguage, setCurrentLanguageState] = useState(() => {
@@ -1696,11 +1600,17 @@ function CodeEditorRoot({
1696
1600
  selectionLength,
1697
1601
  textareaRef: engineReturn.textareaRef,
1698
1602
  placeholder,
1603
+ diffStats: diffAnnotations ? {
1604
+ added: diffAnnotations.added,
1605
+ modified: diffAnnotations.modified,
1606
+ removed: diffAnnotations.removed
1607
+ } : null,
1608
+ _diffAnnotations: diffAnnotations,
1699
1609
  _engineReturn: engineReturn,
1700
1610
  _minHeight: minHeight,
1701
1611
  _maxHeight: maxHeight
1702
1612
  }),
1703
- [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, minHeight, maxHeight]
1613
+ [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight]
1704
1614
  );
1705
1615
  return /* @__PURE__ */ jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
1706
1616
  "div",
@@ -1725,7 +1635,7 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
1725
1635
  });
1726
1636
 
1727
1637
  // src/engine/markdown-render.ts
1728
- function escapeHtml2(s) {
1638
+ function escapeHtml(s) {
1729
1639
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1730
1640
  }
1731
1641
  function escapeAttr(s) {
@@ -1766,10 +1676,10 @@ function renderMarkdown(markdown) {
1766
1676
  i++;
1767
1677
  }
1768
1678
  i++;
1769
- out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml2(lang))}"` : ""}>${escapeHtml2(body.join("\n"))}</code></pre>`);
1679
+ out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml(lang))}"` : ""}>${escapeHtml(body.join("\n"))}</code></pre>`);
1770
1680
  continue;
1771
1681
  }
1772
- const esc = escapeHtml2(line);
1682
+ const esc = escapeHtml(line);
1773
1683
  if (/^\s*$/.test(line)) {
1774
1684
  flushPara();
1775
1685
  closeList();
@@ -1876,44 +1786,6 @@ function MarkdownEditor({
1876
1786
  );
1877
1787
  }
1878
1788
  MarkdownEditor.displayName = "MarkdownEditor";
1879
- var EXT_LANGUAGE = {
1880
- js: "javascript",
1881
- mjs: "javascript",
1882
- cjs: "javascript",
1883
- jsx: "javascript",
1884
- json: "javascript",
1885
- // the JS tokenizer highlights JSON acceptably
1886
- jsonc: "javascript",
1887
- ts: "typescript",
1888
- mts: "typescript",
1889
- cts: "typescript",
1890
- tsx: "typescript",
1891
- html: "html",
1892
- htm: "html",
1893
- xml: "html",
1894
- xhtml: "html",
1895
- vue: "html",
1896
- php: "php",
1897
- phtml: "php",
1898
- py: "python",
1899
- pyw: "python",
1900
- go: "go",
1901
- md: "markdown",
1902
- markdown: "markdown",
1903
- mkd: "markdown",
1904
- mdx: "markdown"
1905
- };
1906
- function basename(filename) {
1907
- return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
1908
- }
1909
- function languageFromFilename(filename) {
1910
- if (!filename) return "plaintext";
1911
- const seg = basename(filename);
1912
- const dot = seg.lastIndexOf(".");
1913
- if (dot <= 0) return "plaintext";
1914
- const ext = seg.slice(dot + 1).toLowerCase();
1915
- return EXT_LANGUAGE[ext] ?? "plaintext";
1916
- }
1917
1789
  function resolveFileKind({ filename, mime }) {
1918
1790
  const media = resolveMediaType({ mime, src: filename });
1919
1791
  if (media !== "unknown") return { kind: "media", mediaKind: media };
@@ -1997,6 +1869,6 @@ function CodeEditorInputTag(props) {
1997
1869
  return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
1998
1870
  }
1999
1871
 
2000
- export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1872
+ export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
2001
1873
  //# sourceMappingURL=index.js.map
2002
1874
  //# sourceMappingURL=index.js.map