draftly 1.0.7 → 2.1.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 (79) hide show
  1. package/README.md +12 -0
  2. package/dist/chunk-3T55CBNZ.cjs +33 -0
  3. package/dist/chunk-3T55CBNZ.cjs.map +1 -0
  4. package/dist/chunk-5MC4T7JH.cjs +58 -0
  5. package/dist/chunk-5MC4T7JH.cjs.map +1 -0
  6. package/dist/{chunk-KBQDZ5IW.cjs → chunk-CLW73JRX.cjs} +100 -75
  7. package/dist/chunk-CLW73JRX.cjs.map +1 -0
  8. package/dist/{chunk-72ZYRGRT.cjs → chunk-EQUQHE2E.cjs} +30 -26
  9. package/dist/chunk-EQUQHE2E.cjs.map +1 -0
  10. package/dist/{chunk-HPSMS2WB.js → chunk-I563H35S.js} +101 -75
  11. package/dist/chunk-I563H35S.js.map +1 -0
  12. package/dist/chunk-IAXF4SJL.js +55 -0
  13. package/dist/chunk-IAXF4SJL.js.map +1 -0
  14. package/dist/chunk-JF3WXXMJ.js +31 -0
  15. package/dist/chunk-JF3WXXMJ.js.map +1 -0
  16. package/dist/{chunk-N3WL3XPB.js → chunk-NRPI5O6Y.js} +2603 -604
  17. package/dist/chunk-NRPI5O6Y.js.map +1 -0
  18. package/dist/{chunk-2B3A3VSQ.cjs → chunk-OMFUE4AQ.cjs} +2642 -622
  19. package/dist/chunk-OMFUE4AQ.cjs.map +1 -0
  20. package/dist/{chunk-DFQYXFOP.js → chunk-TD3L5C45.js} +28 -3
  21. package/dist/chunk-TD3L5C45.js.map +1 -0
  22. package/dist/{chunk-CG4M4TC7.js → chunk-UCHBDJ4R.js} +26 -22
  23. package/dist/chunk-UCHBDJ4R.js.map +1 -0
  24. package/dist/{chunk-KDEDLC3D.cjs → chunk-W75QUUQC.cjs} +29 -2
  25. package/dist/chunk-W75QUUQC.cjs.map +1 -0
  26. package/dist/{draftly-BLnx3uGX.d.cts → draftly-BBL-AdOl.d.cts} +5 -1
  27. package/dist/{draftly-BLnx3uGX.d.ts → draftly-BBL-AdOl.d.ts} +5 -1
  28. package/dist/editor/index.cjs +22 -14
  29. package/dist/editor/index.d.cts +2 -1
  30. package/dist/editor/index.d.ts +2 -1
  31. package/dist/editor/index.js +2 -2
  32. package/dist/index.cjs +65 -39
  33. package/dist/index.d.cts +6 -3
  34. package/dist/index.d.ts +6 -3
  35. package/dist/index.js +6 -4
  36. package/dist/lib/index.cjs +12 -0
  37. package/dist/lib/index.cjs.map +1 -0
  38. package/dist/lib/index.d.cts +16 -0
  39. package/dist/lib/index.d.ts +16 -0
  40. package/dist/lib/index.js +3 -0
  41. package/dist/lib/index.js.map +1 -0
  42. package/dist/plugins/index.cjs +27 -17
  43. package/dist/plugins/index.d.cts +180 -10
  44. package/dist/plugins/index.d.ts +180 -10
  45. package/dist/plugins/index.js +5 -3
  46. package/dist/preview/index.cjs +16 -11
  47. package/dist/preview/index.d.cts +19 -4
  48. package/dist/preview/index.d.ts +19 -4
  49. package/dist/preview/index.js +3 -2
  50. package/package.json +8 -1
  51. package/src/editor/draftly.ts +30 -27
  52. package/src/editor/plugin.ts +5 -1
  53. package/src/editor/theme.ts +1 -0
  54. package/src/editor/utils.ts +33 -0
  55. package/src/index.ts +5 -4
  56. package/src/lib/index.ts +2 -0
  57. package/src/lib/input-handler.ts +45 -0
  58. package/src/plugins/code-plugin.theme.ts +426 -0
  59. package/src/plugins/code-plugin.ts +810 -561
  60. package/src/plugins/emoji-plugin.ts +140 -0
  61. package/src/plugins/index.ts +63 -57
  62. package/src/plugins/inline-plugin.ts +305 -291
  63. package/src/plugins/math-plugin.ts +12 -0
  64. package/src/plugins/table-plugin.ts +1759 -0
  65. package/src/preview/context.ts +4 -1
  66. package/src/preview/css-generator.ts +14 -1
  67. package/src/preview/index.ts +9 -1
  68. package/src/preview/preview.ts +2 -1
  69. package/src/preview/renderer.ts +21 -20
  70. package/src/preview/syntax-theme.ts +110 -0
  71. package/src/preview/types.ts +14 -0
  72. package/dist/chunk-2B3A3VSQ.cjs.map +0 -1
  73. package/dist/chunk-72ZYRGRT.cjs.map +0 -1
  74. package/dist/chunk-CG4M4TC7.js.map +0 -1
  75. package/dist/chunk-DFQYXFOP.js.map +0 -1
  76. package/dist/chunk-HPSMS2WB.js.map +0 -1
  77. package/dist/chunk-KBQDZ5IW.cjs.map +0 -1
  78. package/dist/chunk-KDEDLC3D.cjs.map +0 -1
  79. package/dist/chunk-N3WL3XPB.js.map +0 -1
@@ -1,11 +1,13 @@
1
1
  import { Decoration, EditorView, KeyBinding, WidgetType } from "@codemirror/view";
2
- import { syntaxTree } from "@codemirror/language";
2
+ import { Extension } from "@codemirror/state";
3
+ import { LanguageDescription, syntaxTree } from "@codemirror/language";
3
4
  import { DecorationContext, DecorationPlugin } from "../editor/plugin";
4
- import { createTheme, toggleMarkdownStyle } from "../editor";
5
- import { SyntaxNode } from "@lezer/common";
6
- import { highlightCode } from "@lezer/highlight";
5
+ import { toggleMarkdownStyle } from "../editor";
6
+ import { Parser, SyntaxNode } from "@lezer/common";
7
+ import { Highlighter, highlightCode } from "@lezer/highlight";
7
8
  import { languages } from "@codemirror/language-data";
8
- import { classHighlighter } from "@lezer/highlight";
9
+ import { createWrapSelectionInputHandler } from "../lib";
10
+ import { codePluginTheme as theme } from "./code-plugin.theme";
9
11
 
10
12
  // ============================================================================
11
13
  // Constants
@@ -20,6 +22,21 @@ const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="1
20
22
  /** Delay before resetting copy button state (ms) */
21
23
  const COPY_RESET_DELAY = 2000;
22
24
 
25
+ /** Code fence marker in markdown blocks */
26
+ const CODE_FENCE = "```";
27
+
28
+ /** Regex for quoted code info values like title="file.ts" */
29
+ const QUOTED_INFO_PATTERN = /(\w+)="([^"]*)"/g;
30
+
31
+ /** Regex for /pattern/ with optional instance selectors (/pattern/1-3,5) */
32
+ const TEXT_HIGHLIGHT_PATTERN = /\/([^/]+)\/(?:(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*))?/g;
33
+
34
+ interface PreviewRenderContext {
35
+ sliceDoc(from: number, to: number): string;
36
+ sanitize(html: string): string;
37
+ syntaxHighlighters?: readonly Highlighter[];
38
+ }
39
+
23
40
  // ============================================================================
24
41
  // Decorations
25
42
  // ============================================================================
@@ -40,6 +57,15 @@ const codeMarkDecorations = {
40
57
  // Highlights
41
58
  "code-line-highlight": Decoration.line({ class: "cm-draftly-code-line-highlight" }),
42
59
  "code-text-highlight": Decoration.mark({ class: "cm-draftly-code-text-highlight" }),
60
+
61
+ // Diff preview
62
+ "diff-line-add": Decoration.line({ class: "cm-draftly-code-line-diff-add" }),
63
+ "diff-line-del": Decoration.line({ class: "cm-draftly-code-line-diff-del" }),
64
+ "diff-sign-add": Decoration.mark({ class: "cm-draftly-code-diff-sign-add" }),
65
+ "diff-sign-del": Decoration.mark({ class: "cm-draftly-code-diff-sign-del" }),
66
+ "diff-mod-add": Decoration.mark({ class: "cm-draftly-code-diff-mod-add" }),
67
+ "diff-mod-del": Decoration.mark({ class: "cm-draftly-code-diff-mod-del" }),
68
+ "diff-escape-hidden": Decoration.replace({}),
43
69
  };
44
70
 
45
71
  /**
@@ -62,19 +88,36 @@ export interface CodeBlockProperties {
62
88
  /** Language identifier (first token) */
63
89
  language: string;
64
90
  /** Show line numbers, optionally starting from a specific number */
65
- lineNumbers?: number | boolean;
91
+ showLineNumbers?: number | boolean;
66
92
  /** Title to display */
67
93
  title?: string;
68
94
  /** Caption to display */
69
95
  caption?: string;
70
96
  /** Show copy button */
71
97
  copy?: boolean;
98
+ /** Enable diff preview mode */
99
+ diff?: boolean;
72
100
  /** Lines to highlight (e.g., [2,3,4,5,9]) */
73
101
  highlightLines?: number[];
74
102
  /** Text patterns to highlight with optional instance selection */
75
103
  highlightText?: TextHighlight[];
76
104
  }
77
105
 
106
+ type DiffLineKind = "normal" | "addition" | "deletion";
107
+
108
+ interface DiffLineState {
109
+ kind: DiffLineKind;
110
+ content: string;
111
+ contentOffset: number;
112
+ escapedMarker: boolean;
113
+ modificationRanges?: Array<[number, number]>;
114
+ }
115
+
116
+ interface DiffDisplayLineNumbers {
117
+ oldLine: number | null;
118
+ newLine: number | null;
119
+ }
120
+
78
121
  // ============================================================================
79
122
  // Widgets
80
123
  // ============================================================================
@@ -213,6 +256,7 @@ export class CodePlugin extends DecorationPlugin {
213
256
  readonly version = "1.0.0";
214
257
  override decorationPriority = 25;
215
258
  override readonly requiredNodes = ["InlineCode", "FencedCode", "CodeMark", "CodeInfo", "CodeText"] as const;
259
+ private readonly parserCache = new Map<string, Promise<Parser | null>>();
216
260
 
217
261
  /**
218
262
  * Plugin theme
@@ -239,6 +283,16 @@ export class CodePlugin extends DecorationPlugin {
239
283
  ];
240
284
  }
241
285
 
286
+ /**
287
+ * Intercepts backtick typing to wrap selected text as inline code.
288
+ *
289
+ * If user types '`' while text is selected, wraps each selected range
290
+ * with backticks (selected -> `selected`).
291
+ */
292
+ override getExtensions(): Extension[] {
293
+ return [createWrapSelectionInputHandler({ "`": "`" })];
294
+ }
295
+
242
296
  /**
243
297
  * Toggle code block on current line or selected lines
244
298
  */
@@ -258,8 +312,8 @@ export class CodePlugin extends DecorationPlugin {
258
312
  const nextLine = state.doc.line(nextLineNum);
259
313
 
260
314
  const isWrapped =
261
- prevLine.text.trim().startsWith("```") &&
262
- nextLine.text.trim() === "```" &&
315
+ prevLine.text.trim().startsWith(CODE_FENCE) &&
316
+ nextLine.text.trim() === CODE_FENCE &&
263
317
  prevLineNum !== startLine.number &&
264
318
  nextLineNum !== endLine.number;
265
319
 
@@ -273,8 +327,8 @@ export class CodePlugin extends DecorationPlugin {
273
327
  });
274
328
  } else {
275
329
  // Wrap with code fence
276
- const openFence = "```\n";
277
- const closeFence = "\n```";
330
+ const openFence = `${CODE_FENCE}\n`;
331
+ const closeFence = `\n${CODE_FENCE}`;
278
332
 
279
333
  view.dispatch({
280
334
  changes: [
@@ -306,6 +360,7 @@ export class CodePlugin extends DecorationPlugin {
306
360
  * lineNumbers: 5,
307
361
  * title: "hello.tsx",
308
362
  * copy: true,
363
+ * diff: false,
309
364
  * highlightLines: [2,3,4,5],
310
365
  * highlightText: [{ pattern: "Hello", instances: [3,4,5] }]
311
366
  * }
@@ -320,17 +375,30 @@ export class CodePlugin extends DecorationPlugin {
320
375
 
321
376
  let remaining = codeInfo.trim();
322
377
 
323
- // Extract language (first word before any special tokens)
324
- const langMatch = remaining.match(/^(\w+)/);
325
- if (langMatch && langMatch[1]) {
326
- props.language = langMatch[1];
327
- remaining = remaining.slice(langMatch[0].length).trim();
378
+ // Extract language (first token), but only when it isn't a known directive.
379
+ const firstTokenMatch = remaining.match(/^([^\s]+)/);
380
+ if (firstTokenMatch && firstTokenMatch[1]) {
381
+ const firstToken = firstTokenMatch[1];
382
+ const normalizedToken = firstToken.toLowerCase();
383
+ const isLineNumberDirective = /^(?:line-numbers|linenumbers|showlinenumbers)(?:\{\d+\})?$/.test(
384
+ normalizedToken
385
+ );
386
+ const isKnownDirective =
387
+ isLineNumberDirective ||
388
+ normalizedToken === "copy" ||
389
+ normalizedToken === "diff" ||
390
+ normalizedToken.startsWith("{") ||
391
+ normalizedToken.startsWith("/");
392
+
393
+ if (!isKnownDirective) {
394
+ props.language = firstToken;
395
+ remaining = remaining.slice(firstToken.length).trim();
396
+ }
328
397
  }
329
398
 
330
399
  // Extract quoted values (title="..." caption="...")
331
- const quotedPattern = /(\w+)="([^"]*)"/g;
332
400
  let quotedMatch;
333
- while ((quotedMatch = quotedPattern.exec(remaining)) !== null) {
401
+ while ((quotedMatch = QUOTED_INFO_PATTERN.exec(remaining)) !== null) {
334
402
  const key = quotedMatch[1]?.toLowerCase();
335
403
  const value = quotedMatch[2];
336
404
 
@@ -341,15 +409,16 @@ export class CodePlugin extends DecorationPlugin {
341
409
  }
342
410
  }
343
411
  // Remove matched quoted values
344
- remaining = remaining.replace(quotedPattern, "").trim();
412
+ remaining = remaining.replace(QUOTED_INFO_PATTERN, "").trim();
345
413
 
346
- // Check for line-numbers with optional start value
347
- const lineNumbersMatch = remaining.match(/line-numbers(?:\{(\d+)\})?/);
414
+ // Check for line numbers with optional start value.
415
+ // Supports both `line-numbers` and legacy `showLineNumbers` tokens.
416
+ const lineNumbersMatch = remaining.match(/\b(?:line-numbers|lineNumbers|showLineNumbers)(?:\{(\d+)\})?/i);
348
417
  if (lineNumbersMatch) {
349
418
  if (lineNumbersMatch[1]) {
350
- props.lineNumbers = parseInt(lineNumbersMatch[1], 10);
419
+ props.showLineNumbers = parseInt(lineNumbersMatch[1], 10);
351
420
  } else {
352
- props.lineNumbers = true;
421
+ props.showLineNumbers = true;
353
422
  }
354
423
  remaining = remaining.replace(lineNumbersMatch[0], "").trim();
355
424
  }
@@ -360,28 +429,16 @@ export class CodePlugin extends DecorationPlugin {
360
429
  remaining = remaining.replace(/\bcopy\b/, "").trim();
361
430
  }
362
431
 
432
+ // Check for diff flag
433
+ if (/\bdiff\b/.test(remaining)) {
434
+ props.diff = true;
435
+ remaining = remaining.replace(/\bdiff\b/, "").trim();
436
+ }
437
+
363
438
  // Extract line highlights {2-4,5,9}
364
439
  const lineHighlightMatch = remaining.match(/\{([^}]+)\}/);
365
440
  if (lineHighlightMatch && lineHighlightMatch[1]) {
366
- const highlightLines: number[] = [];
367
- const parts = lineHighlightMatch[1].split(",");
368
-
369
- for (const part of parts) {
370
- const trimmed = part.trim();
371
- const rangeMatch = trimmed.match(/^(\d+)-(\d+)$/);
372
-
373
- if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
374
- // Range: 2-4 -> [2,3,4]
375
- const start = parseInt(rangeMatch[1], 10);
376
- const end = parseInt(rangeMatch[2], 10);
377
- for (let i = start; i <= end; i++) {
378
- highlightLines.push(i);
379
- }
380
- } else if (/^\d+$/.test(trimmed)) {
381
- // Individual number
382
- highlightLines.push(parseInt(trimmed, 10));
383
- }
384
- }
441
+ const highlightLines = this.parseNumberList(lineHighlightMatch[1]);
385
442
 
386
443
  if (highlightLines.length > 0) {
387
444
  props.highlightLines = highlightLines;
@@ -390,11 +447,10 @@ export class CodePlugin extends DecorationPlugin {
390
447
  }
391
448
 
392
449
  // Extract text/regex highlights /pattern/ or /pattern/3-5 or /pattern/3,5
393
- const textHighlightPattern = /\/([^/]+)\/(?:(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*))?/g;
394
450
  let textMatch;
395
451
  const highlightText: TextHighlight[] = [];
396
452
 
397
- while ((textMatch = textHighlightPattern.exec(remaining)) !== null) {
453
+ while ((textMatch = TEXT_HIGHLIGHT_PATTERN.exec(remaining)) !== null) {
398
454
  if (!textMatch[1]) continue;
399
455
  const highlight: TextHighlight = {
400
456
  pattern: textMatch[1],
@@ -402,23 +458,7 @@ export class CodePlugin extends DecorationPlugin {
402
458
 
403
459
  // Parse instance selection if present
404
460
  if (textMatch[2]) {
405
- const instanceStr = textMatch[2];
406
- const instances: number[] = [];
407
- const instanceParts = instanceStr.split(",");
408
-
409
- for (const part of instanceParts) {
410
- const rangeMatch = part.match(/^(\d+)-(\d+)$/);
411
- if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
412
- // Range: 3-5 -> [3,4,5]
413
- const start = parseInt(rangeMatch[1], 10);
414
- const end = parseInt(rangeMatch[2], 10);
415
- for (let i = start; i <= end; i++) {
416
- instances.push(i);
417
- }
418
- } else if (/^\d+$/.test(part)) {
419
- instances.push(parseInt(part, 10));
420
- }
421
- }
461
+ const instances = this.parseNumberList(textMatch[2]);
422
462
 
423
463
  if (instances.length > 0) {
424
464
  highlight.instances = instances;
@@ -440,200 +480,311 @@ export class CodePlugin extends DecorationPlugin {
440
480
  * Handles line numbers, highlights, header/caption widgets, and fence visibility.
441
481
  */
442
482
  buildDecorations(ctx: DecorationContext): void {
443
- const { view, decorations } = ctx;
444
- const tree = syntaxTree(view.state);
483
+ const tree = syntaxTree(ctx.view.state);
445
484
 
446
485
  tree.iterate({
447
486
  enter: (node) => {
448
- const { from, to, name } = node;
449
-
450
- // Handle inline code
451
- if (name === "InlineCode") {
452
- // Add inline code styling
453
- decorations.push(codeMarkDecorations["inline-code"].range(from, to));
454
-
455
- // Hide backticks when cursor is not in range
456
- const cursorInRange = ctx.selectionOverlapsRange(from, to);
457
- if (!cursorInRange) {
458
- // Find the CodeMark children (backticks)
459
- for (let child = node.node.firstChild; child; child = child.nextSibling) {
460
- if (child.name === "CodeMark") {
461
- decorations.push(codeMarkDecorations["inline-mark"].range(child.from, child.to));
462
- }
463
- }
464
- }
487
+ if (node.name === "InlineCode") {
488
+ this.decorateInlineCode(node, ctx);
489
+ return;
465
490
  }
466
491
 
467
- // Handle fenced code blocks
468
- if (name === "FencedCode") {
469
- const nodeLineStart = view.state.doc.lineAt(from);
470
- const nodeLineEnd = view.state.doc.lineAt(to);
471
- const cursorInRange = ctx.selectionOverlapsRange(nodeLineStart.from, nodeLineEnd.to);
472
-
473
- // Extract properties from CodeInfo
474
- let infoProps: CodeBlockProperties = { language: "" };
475
- for (let child = node.node.firstChild; child; child = child.nextSibling) {
476
- if (child.name === "CodeInfo") {
477
- infoProps = this.parseCodeInfo(view.state.sliceDoc(child.from, child.to).trim());
478
- break;
479
- }
480
- }
492
+ if (node.name === "FencedCode") {
493
+ this.decorateFencedCode(node, ctx);
494
+ }
495
+ },
496
+ });
497
+ }
481
498
 
482
- // Calculate line number width for styling (if line numbers enabled)
483
- const totalCodeLines = nodeLineEnd.number - nodeLineStart.number - 1; // Exclude fence lines
484
- const startLineNum = typeof infoProps.lineNumbers === "number" ? infoProps.lineNumbers : 1;
485
- const maxLineNum = startLineNum + totalCodeLines - 1;
486
- const lineNumWidth = Math.max(String(maxLineNum).length, String(startLineNum).length);
487
-
488
- // Track code line index (excluding fence lines)
489
- let codeLineIndex = 0;
490
-
491
- // Extract code content for copy button
492
- let codeContent = "";
493
- for (let child = node.node.firstChild; child; child = child.nextSibling) {
494
- if (child.name === "CodeText") {
495
- codeContent = view.state.sliceDoc(child.from, child.to);
496
- break;
497
- }
498
- }
499
+ private decorateInlineCode(node: { from: number; to: number; node: SyntaxNode }, ctx: DecorationContext): void {
500
+ const { from, to } = node;
501
+ ctx.decorations.push(codeMarkDecorations["inline-code"].range(from, to));
499
502
 
500
- const shouldShowHeader = !cursorInRange && (infoProps.title || infoProps.copy || infoProps.language);
501
- const shouldShowCaption = !cursorInRange && infoProps.caption;
503
+ if (ctx.selectionOverlapsRange(from, to)) {
504
+ return;
505
+ }
502
506
 
503
- // Add header widget when cursor not in range and (title, copy, or language is set)
504
- if (shouldShowHeader) {
505
- decorations.push(
506
- Decoration.widget({
507
- widget: new CodeBlockHeaderWidget(infoProps, codeContent),
508
- block: false,
509
- }).range(nodeLineStart.from)
510
- );
511
- }
507
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
508
+ if (child.name === "CodeMark") {
509
+ ctx.decorations.push(codeMarkDecorations["inline-mark"].range(child.from, child.to));
510
+ }
511
+ }
512
+ }
512
513
 
513
- // Add line decorations for all lines in the block
514
- for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
515
- const line = view.state.doc.line(i);
516
- const isFenceLine = i === nodeLineStart.number || i === nodeLineEnd.number;
517
- const relativeLineNum = startLineNum + codeLineIndex;
518
-
519
- // Base line decoration
520
- decorations.push(codeMarkDecorations["code-block-line"].range(line.from));
521
-
522
- // Add start/end line decorations
523
- if (i === nodeLineStart.number) {
524
- decorations.push(codeMarkDecorations["code-block-line-start"].range(line.from));
525
- // Add class for header presence
526
- if (shouldShowHeader) {
527
- decorations.push(
528
- Decoration.line({
529
- class: "cm-draftly-code-block-has-header",
530
- }).range(line.from)
531
- );
532
- }
533
- }
534
- if (i === nodeLineEnd.number) {
535
- decorations.push(codeMarkDecorations["code-block-line-end"].range(line.from));
536
- // Add class for caption presence
537
- if (shouldShowCaption) {
538
- decorations.push(
539
- Decoration.line({
540
- class: "cm-draftly-code-block-has-caption",
541
- }).range(line.from)
542
- );
543
- }
544
- }
545
-
546
- // Line numbers (only for code lines, not fence lines)
547
- if (!isFenceLine && infoProps.lineNumbers) {
548
- decorations.push(
549
- Decoration.line({
550
- class: "cm-draftly-code-line-numbered",
551
- attributes: {
552
- "data-line-num": String(relativeLineNum),
553
- style: `--line-num-width: ${lineNumWidth}ch`,
554
- },
555
- }).range(line.from)
556
- );
557
- }
558
-
559
- // Line highlight (check if this line should be highlighted)
560
- if (!isFenceLine && infoProps.highlightLines) {
561
- if (infoProps.highlightLines.includes(codeLineIndex + 1)) {
562
- decorations.push(codeMarkDecorations["code-line-highlight"].range(line.from));
563
- }
564
- }
565
-
566
- // Text highlights
567
- if (!isFenceLine && infoProps.highlightText && infoProps.highlightText.length > 0) {
568
- const lineText = view.state.sliceDoc(line.from, line.to);
569
-
570
- for (const textHighlight of infoProps.highlightText) {
571
- try {
572
- const regex = new RegExp(textHighlight.pattern, "g");
573
- let match;
574
- let matchIndex = 0;
575
-
576
- while ((match = regex.exec(lineText)) !== null) {
577
- matchIndex++;
578
-
579
- // Check if this instance should be highlighted
580
- const shouldHighlight = !textHighlight.instances || textHighlight.instances.includes(matchIndex);
581
-
582
- if (shouldHighlight) {
583
- const matchFrom = line.from + match.index;
584
- const matchTo = matchFrom + match[0].length;
585
- decorations.push(codeMarkDecorations["code-text-highlight"].range(matchFrom, matchTo));
586
- }
587
- }
588
- } catch {
589
- // Invalid regex, skip
590
- }
591
- }
592
- }
593
-
594
- // Increment code line index (only for non-fence lines)
595
- if (!isFenceLine) {
596
- codeLineIndex++;
597
- }
598
- }
514
+ private decorateFencedCode(node: { from: number; to: number; node: SyntaxNode }, ctx: DecorationContext): void {
515
+ const { view, decorations } = ctx;
516
+ const nodeLineStart = view.state.doc.lineAt(node.from);
517
+ const nodeLineEnd = view.state.doc.lineAt(node.to);
518
+ const cursorInRange = ctx.selectionOverlapsRange(nodeLineStart.from, nodeLineEnd.to);
599
519
 
600
- // Find CodeMark and CodeInfo children
601
- for (let child = node.node.firstChild; child; child = child.nextSibling) {
602
- if (child.name === "CodeMark" || child.name === "CodeInfo") {
603
- if (cursorInRange) {
604
- // Show fence markers with styling when cursor is in range
605
- decorations.push(codeMarkDecorations["code-fence"].range(child.from, child.to));
606
- } else {
607
- // Hide fence markers when cursor is not in range
608
- decorations.push(codeMarkDecorations["code-hidden"].range(child.from, child.to));
609
- }
610
- }
611
- }
520
+ let infoProps: CodeBlockProperties = { language: "" };
521
+ let codeContent = "";
612
522
 
613
- // Add caption widget when cursor not in range and caption is set
614
- if (!cursorInRange && infoProps.caption) {
615
- decorations.push(
616
- Decoration.widget({
617
- widget: new CodeBlockCaptionWidget(infoProps.caption),
618
- block: false,
619
- side: 1, // After the content
620
- }).range(nodeLineEnd.to)
621
- );
523
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
524
+ if (child.name === "CodeInfo") {
525
+ infoProps = this.parseCodeInfo(view.state.sliceDoc(child.from, child.to).trim());
526
+ }
527
+ if (child.name === "CodeText") {
528
+ codeContent = view.state.sliceDoc(child.from, child.to);
529
+ }
530
+ }
531
+
532
+ const codeLines: string[] = [];
533
+ for (let i = nodeLineStart.number + 1; i <= nodeLineEnd.number - 1; i++) {
534
+ const codeLine = view.state.doc.line(i);
535
+ codeLines.push(view.state.sliceDoc(codeLine.from, codeLine.to));
536
+ }
537
+
538
+ const totalCodeLines = nodeLineEnd.number - nodeLineStart.number - 1;
539
+ const startLineNum = typeof infoProps.showLineNumbers === "number" ? infoProps.showLineNumbers : 1;
540
+ const maxLineNum = startLineNum + totalCodeLines - 1;
541
+ const lineNumWidth = Math.max(String(maxLineNum).length, String(startLineNum).length);
542
+ const highlightInstanceCounters = new Array(infoProps.highlightText?.length ?? 0).fill(0);
543
+
544
+ const diffStates = infoProps.diff ? this.analyzeDiffLines(codeLines) : [];
545
+ const diffDisplayLineNumbers = infoProps.diff ? this.computeDiffDisplayLineNumbers(diffStates, startLineNum) : [];
546
+ const displayLineNumbers = infoProps.diff
547
+ ? diffDisplayLineNumbers.map((numbers, index) => numbers.newLine ?? numbers.oldLine ?? startLineNum + index)
548
+ : codeLines.map((_, index) => startLineNum + index);
549
+ const diffHighlightLineNumbers = infoProps.diff
550
+ ? this.computeDiffDisplayLineNumbers(diffStates, startLineNum).map(
551
+ (numbers, index) => numbers.newLine ?? numbers.oldLine ?? startLineNum + index
552
+ )
553
+ : [];
554
+ const maxOldDiffLineNum = diffDisplayLineNumbers.reduce((max, numbers) => {
555
+ const oldLine = numbers.oldLine ?? 0;
556
+ return oldLine > max ? oldLine : max;
557
+ }, startLineNum);
558
+ const maxNewDiffLineNum = diffDisplayLineNumbers.reduce((max, numbers) => {
559
+ const newLine = numbers.newLine ?? 0;
560
+ return newLine > max ? newLine : max;
561
+ }, startLineNum);
562
+ const diffOldLineNumWidth = Math.max(String(startLineNum).length, String(maxOldDiffLineNum).length);
563
+ const diffNewLineNumWidth = Math.max(String(startLineNum).length, String(maxNewDiffLineNum).length);
564
+
565
+ const shouldShowHeader = !cursorInRange && (infoProps.title || infoProps.copy || infoProps.language);
566
+ const shouldShowCaption = !cursorInRange && !!infoProps.caption;
567
+
568
+ if (shouldShowHeader) {
569
+ decorations.push(
570
+ Decoration.widget({
571
+ widget: new CodeBlockHeaderWidget(infoProps, codeContent),
572
+ block: false,
573
+ side: -1,
574
+ }).range(nodeLineStart.from)
575
+ );
576
+ }
577
+
578
+ let codeLineIndex = 0;
579
+ for (let lineNumber = nodeLineStart.number; lineNumber <= nodeLineEnd.number; lineNumber++) {
580
+ const line = view.state.doc.line(lineNumber);
581
+ const isFenceLine = lineNumber === nodeLineStart.number || lineNumber === nodeLineEnd.number;
582
+ const relativeLineNum = displayLineNumbers[codeLineIndex] ?? startLineNum + codeLineIndex;
583
+
584
+ decorations.push(codeMarkDecorations["code-block-line"].range(line.from));
585
+
586
+ if (lineNumber === nodeLineStart.number) {
587
+ decorations.push(codeMarkDecorations["code-block-line-start"].range(line.from));
588
+ if (shouldShowHeader) {
589
+ decorations.push(Decoration.line({ class: "cm-draftly-code-block-has-header" }).range(line.from));
590
+ }
591
+ }
592
+
593
+ if (lineNumber === nodeLineEnd.number) {
594
+ decorations.push(codeMarkDecorations["code-block-line-end"].range(line.from));
595
+ if (shouldShowCaption) {
596
+ decorations.push(Decoration.line({ class: "cm-draftly-code-block-has-caption" }).range(line.from));
597
+ }
598
+ }
599
+
600
+ if (!isFenceLine && infoProps.showLineNumbers && !infoProps.diff) {
601
+ decorations.push(
602
+ Decoration.line({
603
+ class: "cm-draftly-code-line-numbered",
604
+ attributes: {
605
+ "data-line-num": String(relativeLineNum),
606
+ style: `--line-num-width: ${lineNumWidth}ch`,
607
+ },
608
+ }).range(line.from)
609
+ );
610
+ }
611
+
612
+ if (!isFenceLine && infoProps.showLineNumbers && infoProps.diff) {
613
+ const diffLineNumbers = diffDisplayLineNumbers[codeLineIndex];
614
+ const diffState = diffStates[codeLineIndex];
615
+ const diffMarker = diffState?.kind === "addition" ? "+" : diffState?.kind === "deletion" ? "-" : " ";
616
+ decorations.push(
617
+ Decoration.line({
618
+ class: "cm-draftly-code-line-numbered-diff",
619
+ attributes: {
620
+ "data-line-num-old": diffLineNumbers?.oldLine != null ? String(diffLineNumbers.oldLine) : "",
621
+ "data-line-num-new": diffLineNumbers?.newLine != null ? String(diffLineNumbers.newLine) : "",
622
+ "data-diff-marker": diffMarker,
623
+ style: `--line-num-old-width: ${diffOldLineNumWidth}ch; --line-num-new-width: ${diffNewLineNumWidth}ch`,
624
+ },
625
+ }).range(line.from)
626
+ );
627
+ }
628
+
629
+ if (!isFenceLine && infoProps.diff) {
630
+ this.decorateDiffLine(
631
+ line,
632
+ codeLineIndex,
633
+ diffStates,
634
+ cursorInRange,
635
+ !infoProps.showLineNumbers,
636
+ decorations
637
+ );
638
+ }
639
+
640
+ if (!isFenceLine && infoProps.highlightLines) {
641
+ const highlightLineNumber = infoProps.diff
642
+ ? (diffHighlightLineNumbers[codeLineIndex] ?? codeLineIndex + 1)
643
+ : startLineNum + codeLineIndex;
644
+ if (infoProps.highlightLines.includes(highlightLineNumber)) {
645
+ decorations.push(codeMarkDecorations["code-line-highlight"].range(line.from));
646
+ }
647
+ }
648
+
649
+ if (!isFenceLine && infoProps.highlightText?.length) {
650
+ this.decorateTextHighlights(
651
+ line.from,
652
+ view.state.sliceDoc(line.from, line.to),
653
+ infoProps.highlightText,
654
+ highlightInstanceCounters,
655
+ decorations
656
+ );
657
+ }
658
+
659
+ if (!isFenceLine) {
660
+ codeLineIndex++;
661
+ }
662
+ }
663
+
664
+ this.decorateFenceMarkers(node.node, cursorInRange, decorations);
665
+
666
+ if (!cursorInRange && infoProps.caption) {
667
+ decorations.push(
668
+ Decoration.widget({
669
+ widget: new CodeBlockCaptionWidget(infoProps.caption),
670
+ block: false,
671
+ side: 1,
672
+ }).range(nodeLineEnd.to)
673
+ );
674
+ }
675
+ }
676
+
677
+ private decorateFenceMarkers(
678
+ node: SyntaxNode,
679
+ cursorInRange: boolean,
680
+ decorations: DecorationContext["decorations"]
681
+ ): void {
682
+ for (let child = node.firstChild; child; child = child.nextSibling) {
683
+ if (child.name === "CodeMark" || child.name === "CodeInfo") {
684
+ decorations.push(
685
+ (cursorInRange ? codeMarkDecorations["code-fence"] : codeMarkDecorations["code-hidden"]).range(
686
+ child.from,
687
+ child.to
688
+ )
689
+ );
690
+ }
691
+ }
692
+ }
693
+
694
+ private decorateDiffLine(
695
+ line: { from: number; to: number },
696
+ codeLineIndex: number,
697
+ diffStates: DiffLineState[],
698
+ cursorInRange: boolean,
699
+ showDiffMarkerGutter: boolean,
700
+ decorations: DecorationContext["decorations"]
701
+ ): void {
702
+ const diffState = diffStates[codeLineIndex];
703
+ const diffMarker = diffState?.kind === "addition" ? "+" : diffState?.kind === "deletion" ? "-" : " ";
704
+
705
+ if (showDiffMarkerGutter) {
706
+ decorations.push(
707
+ Decoration.line({
708
+ class: "cm-draftly-code-line-diff-gutter",
709
+ attributes: {
710
+ "data-diff-marker": diffMarker,
711
+ },
712
+ }).range(line.from)
713
+ );
714
+ }
715
+
716
+ if (diffState?.kind === "addition") {
717
+ decorations.push(codeMarkDecorations["diff-line-add"].range(line.from));
718
+ if (cursorInRange && line.to > line.from) {
719
+ decorations.push(codeMarkDecorations["diff-sign-add"].range(line.from, line.from + 1));
720
+ }
721
+ }
722
+
723
+ if (diffState?.kind === "deletion") {
724
+ decorations.push(codeMarkDecorations["diff-line-del"].range(line.from));
725
+ if (cursorInRange && line.to > line.from) {
726
+ decorations.push(codeMarkDecorations["diff-sign-del"].range(line.from, line.from + 1));
727
+ }
728
+ }
729
+
730
+ if (
731
+ !cursorInRange &&
732
+ line.to > line.from &&
733
+ (diffState?.escapedMarker || diffState?.kind === "addition" || diffState?.kind === "deletion")
734
+ ) {
735
+ decorations.push(codeMarkDecorations["diff-escape-hidden"].range(line.from, line.from + 1));
736
+ }
737
+
738
+ if (diffState?.modificationRanges?.length) {
739
+ for (const [start, end] of diffState.modificationRanges) {
740
+ const rangeFrom = line.from + diffState.contentOffset + start;
741
+ const rangeTo = line.from + diffState.contentOffset + end;
742
+ if (rangeTo > rangeFrom) {
743
+ decorations.push(
744
+ (diffState.kind === "addition"
745
+ ? codeMarkDecorations["diff-mod-add"]
746
+ : codeMarkDecorations["diff-mod-del"]
747
+ ).range(rangeFrom, rangeTo)
748
+ );
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ private decorateTextHighlights(
755
+ lineFrom: number,
756
+ lineText: string,
757
+ highlights: TextHighlight[],
758
+ instanceCounters: number[],
759
+ decorations: DecorationContext["decorations"]
760
+ ): void {
761
+ for (const [highlightIndex, textHighlight] of highlights.entries()) {
762
+ try {
763
+ const regex = new RegExp(textHighlight.pattern, "g");
764
+ let match: RegExpExecArray | null;
765
+
766
+ while ((match = regex.exec(lineText)) !== null) {
767
+ instanceCounters[highlightIndex] = (instanceCounters[highlightIndex] ?? 0) + 1;
768
+ const globalMatchIndex = instanceCounters[highlightIndex];
769
+ const shouldHighlight = !textHighlight.instances || textHighlight.instances.includes(globalMatchIndex);
770
+
771
+ if (shouldHighlight) {
772
+ const matchFrom = lineFrom + match.index;
773
+ const matchTo = matchFrom + match[0].length;
774
+ decorations.push(codeMarkDecorations["code-text-highlight"].range(matchFrom, matchTo));
622
775
  }
623
776
  }
624
- },
625
- });
777
+ } catch {
778
+ // Invalid regex; ignore this highlight pattern.
779
+ }
780
+ }
626
781
  }
627
782
 
628
783
  /**
629
784
  * Render code elements to HTML for static preview.
630
785
  * Applies syntax highlighting using @lezer/highlight.
631
786
  */
632
- override renderToHTML(
633
- node: SyntaxNode,
634
- children: string,
635
- ctx: { sliceDoc(from: number, to: number): string; sanitize(html: string): string }
636
- ): string | null {
787
+ override async renderToHTML(node: SyntaxNode, _children: string, ctx: PreviewRenderContext): Promise<string | null> {
637
788
  // Hide CodeMark (backticks)
638
789
  if (node.name === "CodeMark") {
639
790
  return "";
@@ -648,7 +799,7 @@ export class CodePlugin extends DecorationPlugin {
648
799
  if (match && match[1]) {
649
800
  content = match[1];
650
801
  }
651
- return `<code class="cm-draftly-code-inline" style="padding: 0.1rem 0.25rem">${ctx.sanitize(content)}</code>`;
802
+ return `<code class="cm-draftly-code-inline" style="padding: 0.1rem 0.25rem">${this.escapeHtml(content)}</code>`;
652
803
  }
653
804
 
654
805
  // Fenced code block
@@ -680,9 +831,9 @@ export class CodePlugin extends DecorationPlugin {
680
831
  html += `<div class="cm-draftly-code-header">`;
681
832
  html += `<div class="cm-draftly-code-header-left">`;
682
833
  if (props.title) {
683
- html += `<span class="cm-draftly-code-header-title">${ctx.sanitize(props.title)}</span>`;
834
+ html += `<span class="cm-draftly-code-header-title">${this.escapeHtml(props.title)}</span>`;
684
835
  } else if (props.language) {
685
- html += `<span class="cm-draftly-code-header-lang">${ctx.sanitize(props.language)}</span>`;
836
+ html += `<span class="cm-draftly-code-header-lang">${this.escapeHtml(props.language)}</span>`;
686
837
  }
687
838
  html += `</div>`;
688
839
  if (props.copy !== false) {
@@ -699,38 +850,96 @@ export class CodePlugin extends DecorationPlugin {
699
850
  }
700
851
 
701
852
  // Calculate line number info
702
- const startLineNum = typeof props.lineNumbers === "number" ? props.lineNumbers : 1;
703
- const lineNumWidth = String(startLineNum + codeLines.length - 1).length;
853
+ const startLineNum = typeof props.showLineNumbers === "number" ? props.showLineNumbers : 1;
854
+ const previewHighlightCounters = new Array(props.highlightText?.length ?? 0).fill(0);
855
+ const diffStates = props.diff ? this.analyzeDiffLines(codeLines) : [];
856
+ const previewDiffLineNumbers = props.diff ? this.computeDiffDisplayLineNumbers(diffStates, startLineNum) : [];
857
+ const previewLineNumbers = props.diff
858
+ ? previewDiffLineNumbers.map((numbers, index) => numbers.newLine ?? numbers.oldLine ?? startLineNum + index)
859
+ : codeLines.map((_, index) => startLineNum + index);
860
+ const previewHighlightLineNumbers = props.diff
861
+ ? this.computeDiffDisplayLineNumbers(diffStates, startLineNum).map(
862
+ (numbers, index) => numbers.newLine ?? numbers.oldLine ?? startLineNum + index
863
+ )
864
+ : [];
865
+ const lineNumWidth = String(Math.max(...previewLineNumbers, startLineNum)).length;
866
+ const previewOldLineNumWidth = String(
867
+ Math.max(
868
+ ...previewDiffLineNumbers.map((numbers) => numbers.oldLine ?? 0),
869
+ startLineNum
870
+ )
871
+ ).length;
872
+ const previewNewLineNumWidth = String(
873
+ Math.max(
874
+ ...previewDiffLineNumbers.map((numbers) => numbers.newLine ?? 0),
875
+ startLineNum
876
+ )
877
+ ).length;
878
+ const previewContentLines = props.diff ? diffStates.map((state) => state.content) : codeLines;
879
+ const highlightedLines = await this.highlightCodeLines(
880
+ previewContentLines.join("\n"),
881
+ props.language || "",
882
+ ctx.syntaxHighlighters
883
+ );
704
884
 
705
885
  // Code block with line processing
706
886
  const hasHeader = showHeader ? " cm-draftly-code-block-has-header" : "";
707
887
  const hasCaption = props.caption ? " cm-draftly-code-block-has-caption" : "";
708
- html += `<pre class="cm-draftly-code-block${hasHeader}${hasCaption}"${props.language ? ` data-lang="${ctx.sanitize(props.language)}"` : ""}>`;
888
+ html += `<pre class="cm-draftly-code-block${hasHeader}${hasCaption}"${props.language ? ` data-lang="${this.escapeAttribute(props.language)}"` : ""}>`;
709
889
  html += `<code>`;
710
890
 
711
891
  // Process each line
712
892
  codeLines.forEach((line, index) => {
713
- const lineNum = startLineNum + index;
714
- const isHighlighted = props.highlightLines?.includes(index + 1);
893
+ const lineNum = previewLineNumbers[index] ?? startLineNum + index;
894
+ const highlightLineNumber = props.diff
895
+ ? (previewHighlightLineNumbers[index] ?? startLineNum + index)
896
+ : startLineNum + index;
897
+ const isHighlighted = props.highlightLines?.includes(highlightLineNumber);
898
+ const diffState = props.diff ? diffStates[index] : undefined;
899
+ const diffLineNumbers = props.diff ? previewDiffLineNumbers[index] : undefined;
715
900
 
716
901
  // Line classes
717
902
  const lineClasses: string[] = ["cm-draftly-code-line"];
718
903
  if (isHighlighted) lineClasses.push("cm-draftly-code-line-highlight");
719
- if (props.lineNumbers) lineClasses.push("cm-draftly-code-line-numbered");
904
+ if (props.showLineNumbers) {
905
+ lineClasses.push(props.diff ? "cm-draftly-code-line-numbered-diff" : "cm-draftly-code-line-numbered");
906
+ }
907
+ if (diffState?.kind === "addition") lineClasses.push("cm-draftly-code-line-diff-add");
908
+ if (diffState?.kind === "deletion") lineClasses.push("cm-draftly-code-line-diff-del");
720
909
 
721
910
  // Line attributes
722
911
  const lineAttrs: string[] = [`class="${lineClasses.join(" ")}"`];
723
- if (props.lineNumbers) {
912
+ if (props.showLineNumbers && !props.diff) {
724
913
  lineAttrs.push(`data-line-num="${lineNum}"`);
725
914
  lineAttrs.push(`style="--line-num-width: ${lineNumWidth}ch"`);
726
915
  }
916
+ if (props.diff) {
917
+ const diffMarker = diffState?.kind === "addition" ? "+" : diffState?.kind === "deletion" ? "-" : " ";
918
+ if (props.showLineNumbers) {
919
+ lineAttrs.push(`data-line-num-old="${diffLineNumbers?.oldLine != null ? diffLineNumbers.oldLine : ""}"`);
920
+ lineAttrs.push(`data-line-num-new="${diffLineNumbers?.newLine != null ? diffLineNumbers.newLine : ""}"`);
921
+ lineAttrs.push(`data-diff-marker="${diffMarker}"`);
922
+ lineAttrs.push(
923
+ `style="--line-num-old-width: ${previewOldLineNumWidth}ch; --line-num-new-width: ${previewNewLineNumWidth}ch"`
924
+ );
925
+ } else {
926
+ lineAttrs.push(`data-diff-marker="${diffMarker}"`);
927
+ lineClasses.push("cm-draftly-code-line-diff-gutter");
928
+ lineAttrs[0] = `class="${lineClasses.join(" ")}"`;
929
+ }
930
+ }
727
931
 
728
932
  // Highlight text content
729
- let lineContent = this.highlightCodeLine(line, props.language || "", ctx);
933
+ const highlightedLine = highlightedLines[index] ?? this.escapeHtml(previewContentLines[index] ?? line);
934
+ let lineContent = highlightedLine;
935
+
936
+ if (diffState) {
937
+ lineContent = this.renderDiffPreviewLine(diffState, highlightedLine);
938
+ }
730
939
 
731
940
  // Apply text highlights
732
941
  if (props.highlightText && props.highlightText.length > 0) {
733
- lineContent = this.applyTextHighlights(lineContent, props.highlightText);
942
+ lineContent = this.applyTextHighlights(lineContent, props.highlightText, previewHighlightCounters);
734
943
  }
735
944
 
736
945
  html += `<span ${lineAttrs.join(" ")}>${lineContent || " "}</span>`;
@@ -740,7 +949,7 @@ export class CodePlugin extends DecorationPlugin {
740
949
 
741
950
  // Caption
742
951
  if (props.caption) {
743
- html += `<div class="cm-draftly-code-caption">${ctx.sanitize(props.caption)}</div>`;
952
+ html += `<div class="cm-draftly-code-caption">${this.escapeHtml(props.caption)}</div>`;
744
953
  }
745
954
 
746
955
  // Close wrapper container
@@ -757,62 +966,384 @@ export class CodePlugin extends DecorationPlugin {
757
966
  return null;
758
967
  }
759
968
 
969
+ /** Parse comma-separated numbers and ranges (e.g. "1,3-5") into [1,3,4,5]. */
970
+ private parseNumberList(value: string): number[] {
971
+ const result: number[] = [];
972
+
973
+ for (const part of value.split(",")) {
974
+ const trimmed = part.trim();
975
+ const rangeMatch = trimmed.match(/^(\d+)-(\d+)$/);
976
+
977
+ if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
978
+ const start = parseInt(rangeMatch[1], 10);
979
+ const end = parseInt(rangeMatch[2], 10);
980
+ for (let i = start; i <= end; i++) {
981
+ result.push(i);
982
+ }
983
+ continue;
984
+ }
985
+
986
+ if (/^\d+$/.test(trimmed)) {
987
+ result.push(parseInt(trimmed, 10));
988
+ }
989
+ }
990
+
991
+ return result;
992
+ }
993
+
760
994
  /**
761
995
  * Highlight a single line of code using the language's Lezer parser.
762
996
  * Falls back to sanitized plain text if the language is not supported.
763
997
  */
764
- private highlightCodeLine(line: string, lang: string, ctx: { sanitize(html: string): string }): string {
765
- if (!lang || !line) {
766
- return ctx.sanitize(line);
998
+ private async highlightCodeLines(
999
+ code: string,
1000
+ lang: string,
1001
+ syntaxHighlighters?: readonly Highlighter[]
1002
+ ): Promise<string[]> {
1003
+ const rawLines = code.split("\n");
1004
+ if (!lang || !code) {
1005
+ return rawLines.map((line) => this.escapeHtml(line));
767
1006
  }
768
1007
 
769
- // Find the language description
770
- const langDesc = languages.find(
771
- (l) => l.name.toLowerCase() === lang.toLowerCase() || (l.alias && l.alias.includes(lang.toLowerCase()))
772
- );
773
-
774
- if (!langDesc || !langDesc.support) {
775
- return ctx.sanitize(line);
1008
+ const parser = await this.resolveLanguageParser(lang);
1009
+ if (!parser) {
1010
+ return rawLines.map((line) => this.escapeHtml(line));
776
1011
  }
777
1012
 
778
1013
  try {
779
- const parser = langDesc.support.language.parser;
780
- const tree = parser.parse(line);
781
-
782
- let result = "";
1014
+ const tree = parser.parse(code);
1015
+ const highlightedLines: string[] = [""];
783
1016
 
784
1017
  highlightCode(
785
- line,
1018
+ code,
786
1019
  tree,
787
- classHighlighter,
1020
+ syntaxHighlighters && syntaxHighlighters.length > 0 ? syntaxHighlighters : [],
788
1021
  (text, classes) => {
789
- if (classes) {
790
- result += `<span class="${classes}">${ctx.sanitize(text)}</span>`;
791
- } else {
792
- result += ctx.sanitize(text);
793
- }
1022
+ const chunk = classes
1023
+ ? `<span class="${this.escapeAttribute(classes)}">${this.escapeHtml(text)}</span>`
1024
+ : this.escapeHtml(text);
1025
+ highlightedLines[highlightedLines.length - 1] += chunk;
794
1026
  },
795
- () => {} // No newlines for single line
1027
+ () => {
1028
+ highlightedLines.push("");
1029
+ }
796
1030
  );
797
1031
 
798
- return result;
1032
+ return rawLines.map((line, index) => highlightedLines[index] || this.escapeHtml(line));
799
1033
  } catch {
800
- return ctx.sanitize(line);
1034
+ return rawLines.map((line) => this.escapeHtml(line));
1035
+ }
1036
+ }
1037
+
1038
+ private async resolveLanguageParser(lang: string): Promise<Parser | null> {
1039
+ const normalizedLang = this.normalizeLanguage(lang);
1040
+ if (!normalizedLang) return null;
1041
+
1042
+ const cached = this.parserCache.get(normalizedLang);
1043
+ if (cached) return cached;
1044
+
1045
+ const parserPromise = (async () => {
1046
+ const langDesc = LanguageDescription.matchLanguageName(languages, normalizedLang, true);
1047
+
1048
+ if (!langDesc) return null;
1049
+
1050
+ if (langDesc.support) {
1051
+ return langDesc.support.language.parser;
1052
+ }
1053
+
1054
+ if (typeof langDesc.load === "function") {
1055
+ try {
1056
+ const support = await langDesc.load();
1057
+ return support.language.parser;
1058
+ } catch {
1059
+ return null;
1060
+ }
1061
+ }
1062
+
1063
+ return null;
1064
+ })();
1065
+
1066
+ this.parserCache.set(normalizedLang, parserPromise);
1067
+ return parserPromise;
1068
+ }
1069
+
1070
+ private normalizeLanguage(lang: string): string {
1071
+ const normalized = lang.trim().toLowerCase();
1072
+ if (!normalized) return "";
1073
+
1074
+ const normalizedMap: Record<string, string> = {
1075
+ "c++": "cpp",
1076
+ "c#": "csharp",
1077
+ "f#": "fsharp",
1078
+ py: "python",
1079
+ js: "javascript",
1080
+ ts: "typescript",
1081
+ sh: "shell",
1082
+ };
1083
+
1084
+ return normalizedMap[normalized] ?? normalized;
1085
+ }
1086
+
1087
+ private escapeHtml(value: string): string {
1088
+ return value
1089
+ .replace(/&/g, "&amp;")
1090
+ .replace(/</g, "&lt;")
1091
+ .replace(/>/g, "&gt;")
1092
+ .replace(/"/g, "&quot;")
1093
+ .replace(/'/g, "&#39;");
1094
+ }
1095
+
1096
+ private escapeAttribute(value: string): string {
1097
+ return this.escapeHtml(value).replace(/`/g, "&#96;");
1098
+ }
1099
+
1100
+ private analyzeDiffLines(lines: string[]): DiffLineState[] {
1101
+ const states = lines.map((line) => this.parseDiffLineState(line));
1102
+
1103
+ let index = 0;
1104
+ while (index < states.length) {
1105
+ if (states[index]?.kind !== "deletion") {
1106
+ index++;
1107
+ continue;
1108
+ }
1109
+
1110
+ const deletionStart = index;
1111
+ while (index < states.length && states[index]?.kind === "deletion") {
1112
+ index++;
1113
+ }
1114
+ const deletionEnd = index;
1115
+
1116
+ const additionStart = index;
1117
+ while (index < states.length && states[index]?.kind === "addition") {
1118
+ index++;
1119
+ }
1120
+ const additionEnd = index;
1121
+
1122
+ if (additionStart === additionEnd) {
1123
+ continue;
1124
+ }
1125
+
1126
+ const pairCount = Math.min(deletionEnd - deletionStart, additionEnd - additionStart);
1127
+ for (let pairIndex = 0; pairIndex < pairCount; pairIndex++) {
1128
+ const deletionState = states[deletionStart + pairIndex];
1129
+ const additionState = states[additionStart + pairIndex];
1130
+
1131
+ if (!deletionState || !additionState) {
1132
+ continue;
1133
+ }
1134
+
1135
+ const ranges = this.computeChangedRanges(deletionState.content, additionState.content);
1136
+ if (ranges.oldRanges.length > 0) {
1137
+ deletionState.modificationRanges = ranges.oldRanges;
1138
+ }
1139
+ if (ranges.newRanges.length > 0) {
1140
+ additionState.modificationRanges = ranges.newRanges;
1141
+ }
1142
+ }
1143
+ }
1144
+
1145
+ return states;
1146
+ }
1147
+
1148
+ private computeDiffDisplayLineNumbers(states: DiffLineState[], startLineNum: number): DiffDisplayLineNumbers[] {
1149
+ const numbers: DiffDisplayLineNumbers[] = [];
1150
+ let oldLineNumber = startLineNum;
1151
+ let newLineNumber = startLineNum;
1152
+
1153
+ for (const state of states) {
1154
+ if (state.kind === "deletion") {
1155
+ numbers.push({ oldLine: oldLineNumber, newLine: null });
1156
+ oldLineNumber++;
1157
+ continue;
1158
+ }
1159
+
1160
+ if (state.kind === "addition") {
1161
+ numbers.push({ oldLine: null, newLine: newLineNumber });
1162
+ newLineNumber++;
1163
+ continue;
1164
+ }
1165
+
1166
+ numbers.push({ oldLine: oldLineNumber, newLine: newLineNumber });
1167
+ oldLineNumber++;
1168
+ newLineNumber++;
1169
+ }
1170
+
1171
+ return numbers;
1172
+ }
1173
+
1174
+ private parseDiffLineState(line: string): DiffLineState {
1175
+ const escapedMarker = line.startsWith("\\+") || line.startsWith("\\-");
1176
+
1177
+ if (escapedMarker) {
1178
+ return {
1179
+ kind: "normal",
1180
+ content: line.slice(1),
1181
+ contentOffset: 1,
1182
+ escapedMarker: true,
1183
+ };
1184
+ }
1185
+
1186
+ if (line.startsWith("+")) {
1187
+ return {
1188
+ kind: "addition",
1189
+ content: line.slice(1),
1190
+ contentOffset: 1,
1191
+ escapedMarker: false,
1192
+ };
1193
+ }
1194
+
1195
+ if (line.startsWith("-")) {
1196
+ return {
1197
+ kind: "deletion",
1198
+ content: line.slice(1),
1199
+ contentOffset: 1,
1200
+ escapedMarker: false,
1201
+ };
801
1202
  }
1203
+
1204
+ return {
1205
+ kind: "normal",
1206
+ content: line,
1207
+ contentOffset: 0,
1208
+ escapedMarker: false,
1209
+ };
1210
+ }
1211
+
1212
+ private computeChangedRanges(
1213
+ oldText: string,
1214
+ newText: string
1215
+ ): { oldRanges: Array<[number, number]>; newRanges: Array<[number, number]> } {
1216
+ let prefix = 0;
1217
+ while (prefix < oldText.length && prefix < newText.length && oldText[prefix] === newText[prefix]) {
1218
+ prefix++;
1219
+ }
1220
+
1221
+ let oldSuffix = oldText.length;
1222
+ let newSuffix = newText.length;
1223
+ while (oldSuffix > prefix && newSuffix > prefix && oldText[oldSuffix - 1] === newText[newSuffix - 1]) {
1224
+ oldSuffix--;
1225
+ newSuffix--;
1226
+ }
1227
+
1228
+ const oldRanges: Array<[number, number]> = [];
1229
+ const newRanges: Array<[number, number]> = [];
1230
+
1231
+ if (oldSuffix > prefix) {
1232
+ oldRanges.push([prefix, oldSuffix]);
1233
+ }
1234
+ if (newSuffix > prefix) {
1235
+ newRanges.push([prefix, newSuffix]);
1236
+ }
1237
+
1238
+ return { oldRanges, newRanges };
1239
+ }
1240
+
1241
+ private renderDiffPreviewLine(diffState: DiffLineState, highlightedContent: string): string {
1242
+ const modClass =
1243
+ diffState.kind === "addition"
1244
+ ? "cm-draftly-code-diff-mod-add"
1245
+ : diffState.kind === "deletion"
1246
+ ? "cm-draftly-code-diff-mod-del"
1247
+ : "";
1248
+
1249
+ const baseHighlightedContent = highlightedContent || this.escapeHtml(diffState.content);
1250
+
1251
+ const contentHtml =
1252
+ diffState.modificationRanges && modClass
1253
+ ? this.applyRangesToHighlightedHTML(baseHighlightedContent, diffState.modificationRanges, modClass)
1254
+ : baseHighlightedContent;
1255
+
1256
+ return contentHtml || " ";
1257
+ }
1258
+
1259
+ private applyRangesToHighlightedHTML(
1260
+ htmlContent: string,
1261
+ ranges: Array<[number, number]>,
1262
+ className: string
1263
+ ): string {
1264
+ const normalizedRanges = ranges
1265
+ .map(([start, end]) => [Math.max(0, start), Math.max(0, end)] as [number, number])
1266
+ .filter(([start, end]) => end > start)
1267
+ .sort((a, b) => a[0] - b[0]);
1268
+
1269
+ if (normalizedRanges.length === 0 || !htmlContent) {
1270
+ return htmlContent;
1271
+ }
1272
+
1273
+ const isInsideRange = (position: number) => {
1274
+ for (const [start, end] of normalizedRanges) {
1275
+ if (position >= start && position < end) return true;
1276
+ if (position < start) return false;
1277
+ }
1278
+ return false;
1279
+ };
1280
+
1281
+ let result = "";
1282
+ let htmlIndex = 0;
1283
+ let textPosition = 0;
1284
+ let markOpen = false;
1285
+
1286
+ while (htmlIndex < htmlContent.length) {
1287
+ const char = htmlContent[htmlIndex];
1288
+
1289
+ if (char === "<") {
1290
+ const tagEnd = htmlContent.indexOf(">", htmlIndex);
1291
+ if (tagEnd === -1) {
1292
+ result += htmlContent.slice(htmlIndex);
1293
+ break;
1294
+ }
1295
+ result += htmlContent.slice(htmlIndex, tagEnd + 1);
1296
+ htmlIndex = tagEnd + 1;
1297
+ continue;
1298
+ }
1299
+
1300
+ let token = char;
1301
+ if (char === "&") {
1302
+ const entityEnd = htmlContent.indexOf(";", htmlIndex);
1303
+ if (entityEnd !== -1) {
1304
+ token = htmlContent.slice(htmlIndex, entityEnd + 1);
1305
+ htmlIndex = entityEnd + 1;
1306
+ } else {
1307
+ htmlIndex += 1;
1308
+ }
1309
+ } else {
1310
+ htmlIndex += 1;
1311
+ }
1312
+
1313
+ const shouldMark = isInsideRange(textPosition);
1314
+
1315
+ if (shouldMark && !markOpen) {
1316
+ result += `<mark class="${className}">`;
1317
+ markOpen = true;
1318
+ }
1319
+ if (!shouldMark && markOpen) {
1320
+ result += "</mark>";
1321
+ markOpen = false;
1322
+ }
1323
+
1324
+ result += token;
1325
+ textPosition += 1;
1326
+ }
1327
+
1328
+ if (markOpen) {
1329
+ result += "</mark>";
1330
+ }
1331
+
1332
+ return result;
802
1333
  }
803
1334
 
804
1335
  /**
805
1336
  * Apply text highlights (regex patterns) to already syntax-highlighted HTML.
806
1337
  * Wraps matched patterns in `<mark>` elements.
807
1338
  */
808
- private applyTextHighlights(htmlContent: string, highlights: TextHighlight[]): string {
1339
+ private applyTextHighlights(htmlContent: string, highlights: TextHighlight[], instanceCounters?: number[]): string {
809
1340
  let result = htmlContent;
810
1341
 
811
- for (const highlight of highlights) {
1342
+ for (const [highlightIndex, highlight] of highlights.entries()) {
812
1343
  try {
813
1344
  // Create regex from pattern
814
1345
  const regex = new RegExp(`(${highlight.pattern})`, "g");
815
- let matchCount = 0;
1346
+ let matchCount = instanceCounters?.[highlightIndex] ?? 0;
816
1347
 
817
1348
  result = result.replace(regex, (match) => {
818
1349
  matchCount++;
@@ -823,6 +1354,10 @@ export class CodePlugin extends DecorationPlugin {
823
1354
  }
824
1355
  return match;
825
1356
  });
1357
+
1358
+ if (instanceCounters) {
1359
+ instanceCounters[highlightIndex] = matchCount;
1360
+ }
826
1361
  } catch {
827
1362
  // Invalid regex, skip
828
1363
  }
@@ -831,289 +1366,3 @@ export class CodePlugin extends DecorationPlugin {
831
1366
  return result;
832
1367
  }
833
1368
  }
834
-
835
- // ============================================================================
836
- // Theme
837
- // ============================================================================
838
-
839
- /** Theme styles for code elements (light and dark modes) */
840
- const theme = createTheme({
841
- default: {
842
- // Inline code
843
- ".cm-draftly-code-inline": {
844
- fontFamily: "var(--font-jetbrains-mono, monospace)",
845
- fontSize: "0.9rem",
846
- backgroundColor: "rgba(0, 0, 0, 0.05)",
847
- padding: "0.1rem 0.25rem",
848
- border: "1px solid var(--color-border)",
849
- borderRadius: "3px",
850
- },
851
-
852
- // Fenced code block lines
853
- ".cm-draftly-code-block-line": {
854
- "--radius": "0.375rem",
855
-
856
- fontFamily: "var(--font-jetbrains-mono, monospace)",
857
- fontSize: "0.9rem",
858
- backgroundColor: "rgba(0, 0, 0, 0.03)",
859
- padding: "0 1rem !important",
860
- lineHeight: "1.5",
861
- borderLeft: "1px solid var(--color-border)",
862
- borderRight: "1px solid var(--color-border)",
863
- },
864
-
865
- // First line of code block
866
- ".cm-draftly-code-block-line-start": {
867
- borderTopLeftRadius: "var(--radius)",
868
- borderTopRightRadius: "var(--radius)",
869
- position: "relative",
870
- overflow: "hidden",
871
- borderTop: "1px solid var(--color-border)",
872
- paddingBottom: "0.5rem !important",
873
- },
874
-
875
- // Remove top radius when header is present
876
- ".cm-draftly-code-block-has-header": {
877
- padding: "0 !important",
878
- paddingBottom: "0.5rem !important",
879
- },
880
-
881
- // Code block header widget
882
- ".cm-draftly-code-header": {
883
- display: "flex",
884
- justifyContent: "space-between",
885
- alignItems: "center",
886
- padding: "0.25rem 1rem",
887
- backgroundColor: "rgba(0, 0, 0, 0.06)",
888
- fontFamily: "var(--font-jetbrains-mono, monospace)",
889
- fontSize: "0.85rem",
890
- },
891
-
892
- ".cm-draftly-code-header-left": {
893
- display: "flex",
894
- alignItems: "center",
895
- gap: "0.5rem",
896
- },
897
-
898
- ".cm-draftly-code-header-title": {
899
- color: "var(--color-text, inherit)",
900
- fontWeight: "500",
901
- },
902
-
903
- ".cm-draftly-code-header-lang": {
904
- color: "#6a737d",
905
- opacity: "0.8",
906
- },
907
-
908
- ".cm-draftly-code-header-right": {
909
- display: "flex",
910
- alignItems: "center",
911
- gap: "0.5rem",
912
- },
913
-
914
- ".cm-draftly-code-copy-btn": {
915
- display: "flex",
916
- alignItems: "center",
917
- justifyContent: "center",
918
- padding: "0.25rem",
919
- backgroundColor: "transparent",
920
- border: "none",
921
- borderRadius: "4px",
922
- cursor: "pointer",
923
- color: "#6a737d",
924
- transition: "color 0.2s, background-color 0.2s",
925
- },
926
-
927
- ".cm-draftly-code-copy-btn:hover": {
928
- backgroundColor: "rgba(0, 0, 0, 0.1)",
929
- color: "var(--color-text, inherit)",
930
- },
931
-
932
- ".cm-draftly-code-copy-btn.copied": {
933
- color: "#22c55e",
934
- },
935
-
936
- // Caption (below code block)
937
- ".cm-draftly-code-block-has-caption": {
938
- padding: "0 !important",
939
- paddingTop: "0.5rem !important",
940
- },
941
-
942
- ".cm-draftly-code-caption": {
943
- textAlign: "center",
944
- fontSize: "0.85rem",
945
- color: "#6a737d",
946
- fontStyle: "italic",
947
- padding: "0.25rem 1rem",
948
- backgroundColor: "rgba(0, 0, 0, 0.06)",
949
- },
950
-
951
- // Last line of code block
952
- ".cm-draftly-code-block-line-end": {
953
- borderBottomLeftRadius: "var(--radius)",
954
- borderBottomRightRadius: "var(--radius)",
955
- borderBottom: "1px solid var(--color-border)",
956
- paddingTop: "0.5rem !important",
957
- },
958
-
959
- ".cm-draftly-code-block-line-end br": {
960
- display: "none",
961
- },
962
-
963
- // Fence markers (```)
964
- ".cm-draftly-code-fence": {
965
- color: "#6a737d",
966
- fontFamily: "var(--font-jetbrains-mono, monospace)",
967
- },
968
-
969
- // Line numbers
970
- ".cm-draftly-code-line-numbered": {
971
- paddingLeft: "calc(var(--line-num-width, 2ch) + 1rem) !important",
972
- position: "relative",
973
- },
974
-
975
- ".cm-draftly-code-line-numbered::before": {
976
- content: "attr(data-line-num)",
977
- position: "absolute",
978
- left: "0.5rem",
979
- top: "0.2rem",
980
- width: "var(--line-num-width, 2ch)",
981
- textAlign: "right",
982
- color: "#6a737d",
983
- opacity: "0.6",
984
- fontFamily: "var(--font-jetbrains-mono, monospace)",
985
- fontSize: "0.85rem",
986
- userSelect: "none",
987
- },
988
-
989
- // Preview: code lines (need block display for full-width highlights)
990
- ".cm-draftly-code-line": {
991
- display: "block",
992
- position: "relative",
993
- paddingLeft: "1rem",
994
- paddingRight: "1rem",
995
- lineHeight: "1.5",
996
- borderLeft: "3px solid transparent",
997
- },
998
-
999
- // Line highlight
1000
- ".cm-draftly-code-line-highlight": {
1001
- backgroundColor: "rgba(255, 220, 100, 0.2) !important",
1002
- borderLeft: "3px solid #f0b429 !important",
1003
- },
1004
-
1005
- // Text highlight
1006
- ".cm-draftly-code-text-highlight": {
1007
- backgroundColor: "rgba(255, 220, 100, 0.4)",
1008
- borderRadius: "2px",
1009
- padding: "0.1rem 0",
1010
- },
1011
- // Preview: container wrapper
1012
- ".cm-draftly-code-container": {
1013
- margin: "1rem 0",
1014
- borderRadius: "var(--radius)",
1015
- overflow: "hidden",
1016
- border: "1px solid var(--color-border)",
1017
- },
1018
-
1019
- // Preview: header inside container
1020
- ".cm-draftly-code-container .cm-draftly-code-header": {
1021
- borderRadius: "0",
1022
- border: "none",
1023
- borderBottom: "1px solid var(--color-border)",
1024
- },
1025
-
1026
- // Preview: code block inside container
1027
- ".cm-draftly-code-container .cm-draftly-code-block": {
1028
- margin: "0",
1029
- borderRadius: "0",
1030
- border: "none",
1031
- whiteSpace: "pre-wrap",
1032
- },
1033
-
1034
- // Preview: caption inside container
1035
- ".cm-draftly-code-container .cm-draftly-code-caption": {
1036
- borderTop: "1px solid var(--color-border)",
1037
- },
1038
-
1039
- // Preview: standalone code block (not in container)
1040
- ".cm-draftly-code-block": {
1041
- fontFamily: "var(--font-jetbrains-mono, monospace)",
1042
- fontSize: "0.9rem",
1043
- backgroundColor: "rgba(0, 0, 0, 0.03)",
1044
- padding: "1rem",
1045
- overflow: "auto",
1046
- position: "relative",
1047
- borderRadius: "var(--radius)",
1048
- border: "1px solid var(--color-border)",
1049
- },
1050
-
1051
- // Preview: code block with header (remove top radius)
1052
- ".cm-draftly-code-block.cm-draftly-code-block-has-header": {
1053
- borderTopLeftRadius: "0",
1054
- borderTopRightRadius: "0",
1055
- borderTop: "none",
1056
- margin: "0",
1057
- paddingTop: "0.5rem !important",
1058
- },
1059
-
1060
- // Preview: code block with caption (remove bottom radius)
1061
- ".cm-draftly-code-block.cm-draftly-code-block-has-caption": {
1062
- borderBottomLeftRadius: "0",
1063
- borderBottomRightRadius: "0",
1064
- borderBottom: "none",
1065
- paddingBottom: "0.5rem !important",
1066
- },
1067
- },
1068
-
1069
- dark: {
1070
- ".cm-draftly-code-inline": {
1071
- backgroundColor: "rgba(255, 255, 255, 0.1)",
1072
- },
1073
-
1074
- ".cm-draftly-code-block-line": {
1075
- backgroundColor: "rgba(255, 255, 255, 0.05)",
1076
- },
1077
-
1078
- ".cm-draftly-code-fence": {
1079
- color: "#8b949e",
1080
- },
1081
-
1082
- ".cm-draftly-code-block": {
1083
- backgroundColor: "rgba(255, 255, 255, 0.05)",
1084
- },
1085
-
1086
- ".cm-draftly-code-header": {
1087
- backgroundColor: "rgba(255, 255, 255, 0.08)",
1088
- },
1089
-
1090
- ".cm-draftly-code-header-lang": {
1091
- color: "#8b949e",
1092
- },
1093
-
1094
- ".cm-draftly-code-copy-btn": {
1095
- color: "#8b949e",
1096
- },
1097
-
1098
- ".cm-draftly-code-copy-btn:hover": {
1099
- backgroundColor: "rgba(255, 255, 255, 0.1)",
1100
- },
1101
-
1102
- ".cm-draftly-code-caption": {
1103
- backgroundColor: "rgba(255, 255, 255, 0.05)",
1104
- },
1105
-
1106
- ".cm-draftly-code-line-numbered::before": {
1107
- color: "#8b949e",
1108
- },
1109
-
1110
- ".cm-draftly-code-line-highlight": {
1111
- backgroundColor: "rgba(255, 220, 100, 0.15) !important",
1112
- borderLeft: "3px solid #d9a520 !important",
1113
- },
1114
-
1115
- ".cm-draftly-code-text-highlight": {
1116
- backgroundColor: "rgba(255, 220, 100, 0.3)",
1117
- },
1118
- },
1119
- });