leepi 0.0.0 → 0.0.3

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 (86) hide show
  1. package/dist/core/active-marks.d.ts +15 -0
  2. package/dist/core/active-marks.js +57 -0
  3. package/dist/core/commands.d.ts +39 -0
  4. package/dist/core/commands.js +415 -0
  5. package/dist/core/editor.d.ts +25 -0
  6. package/dist/core/editor.js +102 -0
  7. package/dist/core/field-notifier.d.ts +21 -0
  8. package/dist/core/field-notifier.js +56 -0
  9. package/dist/core/highlight-style.js +60 -0
  10. package/dist/core/highlight.d.ts +8 -0
  11. package/dist/core/highlight.js +34 -0
  12. package/dist/core/plugins/blockquote.d.ts +11 -0
  13. package/dist/core/plugins/blockquote.js +78 -0
  14. package/dist/core/plugins/bracket.d.ts +6 -0
  15. package/dist/core/plugins/bracket.js +38 -0
  16. package/dist/core/plugins/code-block.d.ts +27 -0
  17. package/dist/core/plugins/code-block.js +207 -0
  18. package/dist/core/plugins/heading.d.ts +13 -0
  19. package/dist/core/plugins/heading.js +111 -0
  20. package/dist/core/plugins/inline.d.ts +14 -0
  21. package/dist/core/plugins/inline.js +103 -0
  22. package/dist/core/plugins/link.d.ts +25 -0
  23. package/dist/core/plugins/link.js +104 -0
  24. package/dist/core/plugins/list.d.ts +14 -0
  25. package/dist/core/plugins/list.js +91 -0
  26. package/dist/core/plugins/table.d.ts +12 -0
  27. package/dist/core/plugins/table.js +161 -0
  28. package/dist/core/plugins.d.ts +9 -0
  29. package/dist/core/plugins.js +9 -0
  30. package/dist/core/popover.d.ts +9 -0
  31. package/dist/core/popover.js +16 -0
  32. package/dist/core/registry.d.ts +10 -0
  33. package/dist/core/registry.js +8 -0
  34. package/dist/core/types.d.ts +25 -0
  35. package/dist/core/types.js +0 -0
  36. package/dist/core/utils.d.ts +13 -0
  37. package/dist/core/utils.js +32 -0
  38. package/dist/leepi.css +461 -0
  39. package/dist/react/code-block-popover.d.ts +76 -0
  40. package/dist/react/code-block-popover.js +223 -0
  41. package/dist/react/context.d.ts +42 -0
  42. package/dist/react/context.js +88 -0
  43. package/dist/react/editor.d.ts +30 -0
  44. package/dist/react/editor.js +60 -0
  45. package/dist/react/floating-toolbar.d.ts +30 -0
  46. package/dist/react/floating-toolbar.js +87 -0
  47. package/dist/react/link-popover.d.ts +70 -0
  48. package/dist/react/link-popover.js +222 -0
  49. package/dist/react/preview.d.ts +13 -0
  50. package/dist/react/preview.js +56 -0
  51. package/dist/react/toolbar.d.ts +51 -0
  52. package/dist/react/toolbar.js +161 -0
  53. package/package.json +90 -1
  54. package/src/core/active-marks.ts +89 -0
  55. package/src/core/commands.ts +461 -0
  56. package/src/core/editor.ts +139 -0
  57. package/src/core/field-notifier.ts +71 -0
  58. package/src/core/highlight-style.ts +66 -0
  59. package/src/core/highlight.ts +50 -0
  60. package/src/core/plugins/blockquote.ts +108 -0
  61. package/src/core/plugins/bracket.ts +34 -0
  62. package/src/core/plugins/code-block.ts +195 -0
  63. package/src/core/plugins/heading.ts +95 -0
  64. package/src/core/plugins/index.ts +16 -0
  65. package/src/core/plugins/inline.ts +62 -0
  66. package/src/core/plugins/link.ts +124 -0
  67. package/src/core/plugins/list.ts +68 -0
  68. package/src/core/plugins/table.ts +217 -0
  69. package/src/core/popover.ts +17 -0
  70. package/src/core/registry.ts +18 -0
  71. package/src/core/types.ts +25 -0
  72. package/src/core/utils.ts +38 -0
  73. package/src/react/code-block-popover.tsx +387 -0
  74. package/src/react/context.tsx +153 -0
  75. package/src/react/editor.tsx +106 -0
  76. package/src/react/floating-toolbar.tsx +161 -0
  77. package/src/react/link-popover.tsx +354 -0
  78. package/src/react/preview.tsx +80 -0
  79. package/src/react/toolbar.tsx +294 -0
  80. package/src/styles/floating-toolbar.css +52 -0
  81. package/src/styles/leepi.css +2 -0
  82. package/src/styles/popover.css +93 -0
  83. package/src/styles/preview.css +191 -0
  84. package/src/styles/theme.css +99 -0
  85. package/src/styles/tokens.css +63 -0
  86. package/src/styles/toolbar.css +55 -0
@@ -0,0 +1,66 @@
1
+ import { type Extension } from "@codemirror/state";
2
+ import { EditorView } from "@codemirror/view";
3
+ import { HighlightStyle } from "@codemirror/language";
4
+ import { tags } from "@lezer/highlight";
5
+
6
+ /** Shared highlight style for faded markdown syntax tokens (e.g. `#`, `>`, `-`, `` ` ``). */
7
+ export const highlightStyle: HighlightStyle = HighlightStyle.define([
8
+ {
9
+ tag: tags.processingInstruction,
10
+ color: "var(--lp-color-text-faded)",
11
+ opacity: "0.4",
12
+ fontFamily: "var(--lp-font-mono)",
13
+ },
14
+ {
15
+ tag: tags.meta,
16
+ color: "var(--lp-color-text-faded)",
17
+ opacity: "0.4",
18
+ fontFamily: "var(--lp-font-mono)",
19
+ },
20
+ {
21
+ tag: tags.labelName,
22
+ color: "var(--lp-color-text-faded)",
23
+ opacity: "0.4",
24
+ fontFamily: "var(--lp-font-mono)",
25
+ },
26
+ {
27
+ tag: tags.contentSeparator,
28
+ color: "var(--lp-color-text-faded)",
29
+ opacity: "0.4",
30
+ fontFamily: "var(--lp-font-mono)",
31
+ },
32
+ ]);
33
+
34
+ export const editorTheme: Extension = EditorView.theme({
35
+ "&": {
36
+ height: "100%",
37
+ flex: "1",
38
+ fontSize: "var(--lp-font-size, 0.875rem)",
39
+ fontFamily: "var(--lp-font-body, system-ui, -apple-system, sans-serif)",
40
+ },
41
+ ".cm-scroller": {
42
+ overflow: "auto",
43
+ fontFamily: "inherit",
44
+ lineHeight: "var(--lp-line-height, 1.6)",
45
+ },
46
+ ".cm-content": {
47
+ maxWidth: "var(--lp-content-max-width, 48rem)",
48
+ margin: "0 auto",
49
+ padding: "1.5rem var(--lp-content-padding, 2rem)",
50
+ caretColor: "var(--lp-color-caret, currentColor)",
51
+ },
52
+ "&.cm-focused": {
53
+ outline: "none",
54
+ },
55
+ ".cm-activeLine": {
56
+ backgroundColor: "transparent",
57
+ },
58
+ ".cm-gutters": {
59
+ display: "none",
60
+ },
61
+ ".cm-has-deco span[class]": {
62
+ backgroundColor: "transparent",
63
+ padding: "0",
64
+ borderRadius: "0",
65
+ },
66
+ });
@@ -0,0 +1,50 @@
1
+ import { classHighlighter, highlightTree } from "@lezer/highlight";
2
+ import { LanguageDescription } from "@codemirror/language";
3
+ import type { LanguageDescription as LanguageDescType } from "@codemirror/language";
4
+
5
+ let languageList: readonly LanguageDescType[] | null = null;
6
+
7
+ async function getLanguages() {
8
+ if (languageList) return languageList;
9
+ const { languages } = await import("@codemirror/language-data");
10
+ languageList = languages;
11
+ return languages;
12
+ }
13
+
14
+ /**
15
+ * Highlight code using Lezer parsers (same ones CodeMirror uses).
16
+ * Returns HTML string with `tok-*` classes for styling.
17
+ */
18
+ export async function highlightCode(code: string, lang: string): Promise<string> {
19
+ const languages = await getLanguages();
20
+ const desc = LanguageDescription.matchLanguageName(languages, lang, true);
21
+ if (!desc) return escapeHtml(code);
22
+
23
+ const support = await desc.load();
24
+ const tree = support.language.parser.parse(code);
25
+
26
+ let pos = 0;
27
+ let result = "";
28
+
29
+ highlightTree(tree, classHighlighter, (from, to, classes) => {
30
+ if (from > pos) {
31
+ result += escapeHtml(code.slice(pos, from));
32
+ }
33
+ result += `<span class="${classes}">${escapeHtml(code.slice(from, to))}</span>`;
34
+ pos = to;
35
+ });
36
+
37
+ if (pos < code.length) {
38
+ result += escapeHtml(code.slice(pos));
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ function escapeHtml(text: string): string {
45
+ return text
46
+ .replace(/&/g, "&amp;")
47
+ .replace(/</g, "&lt;")
48
+ .replace(/>/g, "&gt;")
49
+ .replace(/"/g, "&quot;");
50
+ }
@@ -0,0 +1,108 @@
1
+ import type { Extension } from "@codemirror/state";
2
+ import { RangeSetBuilder } from "@codemirror/state";
3
+ import {
4
+ Decoration,
5
+ type DecorationSet,
6
+ EditorView,
7
+ ViewPlugin,
8
+ type ViewUpdate,
9
+ keymap,
10
+ } from "@codemirror/view";
11
+ import type { EditorState } from "@codemirror/state";
12
+ import { HighlightStyle, syntaxHighlighting, syntaxTree } from "@codemirror/language";
13
+ import { tags } from "@lezer/highlight";
14
+ import { shortcutRegistry, markRegistry } from "../registry";
15
+ import { toggleBlockquote } from "../commands";
16
+
17
+ export interface BlockquotePluginOptions {
18
+ shortcuts?: {
19
+ blockquote?: string;
20
+ };
21
+ }
22
+
23
+ const defaultShortcuts = {
24
+ blockquote: "Mod-Shift-b",
25
+ };
26
+
27
+ // --- Blockquote line decorations ---
28
+
29
+ const blockquoteDecoration = Decoration.line({ class: "cm-blockquote-line" });
30
+
31
+ function buildBlockquoteDecorations(state: EditorState): DecorationSet {
32
+ const builder = new RangeSetBuilder<Decoration>();
33
+ const tree = syntaxTree(state);
34
+ const seen = new Set<number>();
35
+
36
+ tree.iterate({
37
+ enter(node) {
38
+ if (node.name === "Blockquote") {
39
+ const from = state.doc.lineAt(node.from).number;
40
+ const to = state.doc.lineAt(node.to).number;
41
+ for (let i = from; i <= to; i++) {
42
+ if (!seen.has(i)) {
43
+ seen.add(i);
44
+ const line = state.doc.line(i);
45
+ builder.add(line.from, line.from, blockquoteDecoration);
46
+ }
47
+ }
48
+ }
49
+ },
50
+ });
51
+
52
+ return builder.finish();
53
+ }
54
+
55
+ const blockquoteLineDecorations = ViewPlugin.fromClass(
56
+ class {
57
+ decorations: DecorationSet;
58
+ constructor(view: EditorView) {
59
+ this.decorations = buildBlockquoteDecorations(view.state);
60
+ }
61
+ update(update: ViewUpdate) {
62
+ if (
63
+ update.docChanged ||
64
+ update.viewportChanged ||
65
+ syntaxTree(update.state) !== syntaxTree(update.startState)
66
+ ) {
67
+ this.decorations = buildBlockquoteDecorations(update.state);
68
+ }
69
+ }
70
+ },
71
+ { decorations: (v) => v.decorations },
72
+ );
73
+
74
+ const blockquoteTheme = EditorView.theme({
75
+ ".cm-blockquote-line": {
76
+ position: "relative",
77
+ },
78
+ ".cm-blockquote-line::before": {
79
+ content: '""',
80
+ position: "absolute",
81
+ right: "calc(100% + 0.5ch)",
82
+ top: "0",
83
+ bottom: "0",
84
+ width: "3px",
85
+ backgroundColor: "var(--lp-color-border, rgba(0, 0, 0, 0.12))",
86
+ pointerEvents: "none",
87
+ },
88
+ });
89
+
90
+ const blockquoteHighlight = HighlightStyle.define([
91
+ {
92
+ tag: tags.quote,
93
+ color: "var(--lp-color-text-secondary)",
94
+ },
95
+ ]);
96
+
97
+ export function blockquotePlugin(options?: BlockquotePluginOptions): Extension {
98
+ const keys = { ...defaultShortcuts, ...options?.shortcuts };
99
+
100
+ return [
101
+ syntaxHighlighting(blockquoteHighlight),
102
+ shortcutRegistry.of([{ key: keys.blockquote, action: "blockquote", plugin: "blockquote" }]),
103
+ markRegistry.of([{ mark: "blockquote", detect: (nodeName) => nodeName === "Blockquote" }]),
104
+ keymap.of([{ key: keys.blockquote, run: toggleBlockquote }]),
105
+ blockquoteLineDecorations,
106
+ blockquoteTheme,
107
+ ];
108
+ }
@@ -0,0 +1,34 @@
1
+ import { type Extension, EditorSelection } from "@codemirror/state";
2
+ import { EditorView } from "@codemirror/view";
3
+
4
+ const BRACKETS: Record<string, string> = {
5
+ "(": ")",
6
+ "[": "]",
7
+ "{": "}",
8
+ };
9
+
10
+ export function bracketPlugin(): Extension {
11
+ return EditorView.inputHandler.of((view, from, to, insert) => {
12
+ const close = BRACKETS[insert];
13
+ if (!close) return false;
14
+
15
+ const range = view.state.selection.main;
16
+
17
+ if (range.empty) {
18
+ // Auto-insert closing bracket and place cursor between them
19
+ view.dispatch({
20
+ changes: { from, to, insert: insert + close },
21
+ selection: EditorSelection.cursor(from + 1),
22
+ });
23
+ return true;
24
+ }
25
+
26
+ // Wrap selected text with brackets
27
+ const selected = view.state.sliceDoc(range.from, range.to);
28
+ view.dispatch({
29
+ changes: { from: range.from, to: range.to, insert: insert + selected + close },
30
+ selection: EditorSelection.range(range.from + 1, range.from + 1 + selected.length),
31
+ });
32
+ return true;
33
+ });
34
+ }
@@ -0,0 +1,195 @@
1
+ import { type Extension, RangeSetBuilder } from "@codemirror/state";
2
+ import {
3
+ Decoration,
4
+ type DecorationSet,
5
+ EditorView,
6
+ ViewPlugin,
7
+ type ViewUpdate,
8
+ keymap,
9
+ } from "@codemirror/view";
10
+ import type { EditorState } from "@codemirror/state";
11
+ import { HighlightStyle, syntaxHighlighting, syntaxTree } from "@codemirror/language";
12
+ import { tags } from "@lezer/highlight";
13
+ import { shortcutRegistry, markRegistry } from "../registry";
14
+ import { findCodeFenceAtCursor, applyCodeBlock } from "../commands";
15
+ import { openPopover } from "../popover";
16
+ import type { PopoverRequest } from "../types";
17
+
18
+ export interface CodeBlockData {
19
+ /** Pre-filled language */
20
+ lang: string;
21
+ /** Pre-filled filename */
22
+ filename: string;
23
+ /** Range of the opening fence line to replace */
24
+ fenceFrom: number;
25
+ fenceTo: number;
26
+ /** Whether this is editing an existing block or inserting a new one */
27
+ isNew: boolean;
28
+ /** For new blocks: the cursor position where the block should be inserted */
29
+ insertPos?: number;
30
+ }
31
+
32
+ export type CodeBlockRequest = PopoverRequest<"codeblock", CodeBlockData>;
33
+
34
+ // --- Code block line number decorations ---
35
+
36
+ function buildCodeLineNumbers(state: EditorState) {
37
+ const builder = new RangeSetBuilder<Decoration>();
38
+ const tree = syntaxTree(state);
39
+ const doc = state.doc;
40
+
41
+ tree.iterate({
42
+ enter(node) {
43
+ if (node.name !== "FencedCode") return;
44
+ const startLine = doc.lineAt(node.from);
45
+ const endLine = doc.lineAt(node.to);
46
+ const totalLines = endLine.number - startLine.number - 1;
47
+ const digits = String(totalLines).length;
48
+ let num = 1;
49
+ for (let l = startLine.number + 1; l < endLine.number; l++) {
50
+ const line = doc.line(l);
51
+ const attributes: Record<string, string> = {
52
+ "data-line-num": String(num++),
53
+ };
54
+ if (digits > 3) {
55
+ attributes.style = `--line-digits:${digits}`;
56
+ }
57
+ builder.add(line.from, line.from, Decoration.line({ class: "cm-has-deco", attributes }));
58
+ }
59
+ },
60
+ });
61
+
62
+ return builder.finish();
63
+ }
64
+
65
+ const codeBlockLineNumbers = ViewPlugin.fromClass(
66
+ class {
67
+ decorations: DecorationSet;
68
+ constructor(view: EditorView) {
69
+ this.decorations = buildCodeLineNumbers(view.state);
70
+ }
71
+ update(update: ViewUpdate) {
72
+ if (
73
+ update.docChanged ||
74
+ update.viewportChanged ||
75
+ syntaxTree(update.state) !== syntaxTree(update.startState)
76
+ ) {
77
+ this.decorations = buildCodeLineNumbers(update.state);
78
+ }
79
+ }
80
+ },
81
+ { decorations: (v) => v.decorations },
82
+ );
83
+
84
+ const codeBlockTheme = EditorView.theme({
85
+ ".cm-has-deco": {
86
+ position: "relative",
87
+ fontSize: "0.875em",
88
+ fontFamily: "var(--lp-font-mono, ui-monospace, monospace)",
89
+ },
90
+ ".cm-has-deco::before": {
91
+ content: "attr(data-line-num)",
92
+ position: "absolute",
93
+ right: "calc(100% + 0.5ch)",
94
+ width: "calc(var(--line-digits, 3) * 1ch)",
95
+ textAlign: "right",
96
+ color: "var(--lp-color-text-faded)",
97
+ opacity: "0.5",
98
+ borderRadius: "3px",
99
+ padding: "0 0.3ch",
100
+ userSelect: "none",
101
+ font: "inherit",
102
+ pointerEvents: "none",
103
+ },
104
+ });
105
+
106
+ const codeHighlight = HighlightStyle.define([
107
+ { tag: tags.keyword, color: "var(--lp-syntax-keyword)" },
108
+ { tag: tags.operator, color: "var(--lp-syntax-operator)" },
109
+ { tag: tags.variableName, color: "var(--lp-syntax-variable)" },
110
+ { tag: tags.function(tags.variableName), color: "var(--lp-syntax-function)" },
111
+ { tag: tags.definition(tags.variableName), color: "var(--lp-syntax-type)" },
112
+ { tag: tags.typeName, color: "var(--lp-syntax-type)" },
113
+ { tag: tags.className, color: "var(--lp-syntax-type)" },
114
+ { tag: tags.string, color: "var(--lp-syntax-string)" },
115
+ { tag: tags.number, color: "var(--lp-syntax-number)" },
116
+ { tag: tags.bool, color: "var(--lp-syntax-number)" },
117
+ { tag: tags.null, color: "var(--lp-syntax-number)" },
118
+ { tag: tags.comment, color: "var(--lp-syntax-comment)", fontStyle: "italic" },
119
+ { tag: tags.propertyName, color: "var(--lp-syntax-property)" },
120
+ { tag: tags.function(tags.propertyName), color: "var(--lp-syntax-function)" },
121
+ { tag: tags.definition(tags.propertyName), color: "var(--lp-syntax-function)" },
122
+ { tag: tags.tagName, color: "var(--lp-syntax-tag)" },
123
+ { tag: tags.attributeName, color: "var(--lp-syntax-attribute)" },
124
+ { tag: tags.attributeValue, color: "var(--lp-syntax-string)" },
125
+ { tag: tags.regexp, color: "var(--lp-syntax-string)" },
126
+ { tag: tags.escape, color: "var(--lp-syntax-escape)" },
127
+ { tag: tags.punctuation, color: "var(--lp-syntax-punctuation)" },
128
+ ]);
129
+
130
+ export interface CodeBlockPluginOptions {
131
+ shortcuts?: {
132
+ codeBlock?: string;
133
+ };
134
+ }
135
+
136
+ const defaultShortcuts = {
137
+ codeBlock: "Mod-Shift-e",
138
+ };
139
+
140
+ export function codeBlockPlugin(options?: CodeBlockPluginOptions): Extension {
141
+ const keys = { ...defaultShortcuts, ...options?.shortcuts };
142
+
143
+ return [
144
+ syntaxHighlighting(codeHighlight),
145
+ shortcutRegistry.of([{ key: keys.codeBlock, action: "codeblock", plugin: "code-block" }]),
146
+ markRegistry.of([{ mark: "codeblock", detect: (nodeName) => nodeName === "FencedCode" }]),
147
+ keymap.of([
148
+ {
149
+ key: keys.codeBlock,
150
+ run(view) {
151
+ const { state } = view;
152
+ const range = state.selection.main;
153
+ const coords = view.coordsAtPos(range.from);
154
+ if (!coords) return false;
155
+
156
+ const existing = findCodeFenceAtCursor(view);
157
+ const req: CodeBlockRequest = existing
158
+ ? {
159
+ type: "codeblock",
160
+ x: coords.left,
161
+ y: coords.bottom,
162
+ data: {
163
+ lang: existing.lang,
164
+ filename: existing.filename,
165
+ fenceFrom: existing.fenceLine.from,
166
+ fenceTo: existing.fenceLine.to,
167
+ isNew: false,
168
+ },
169
+ }
170
+ : {
171
+ type: "codeblock",
172
+ x: coords.left,
173
+ y: coords.bottom,
174
+ data: {
175
+ lang: "",
176
+ filename: "",
177
+ fenceFrom: 0,
178
+ fenceTo: 0,
179
+ isNew: true,
180
+ insertPos: range.from,
181
+ },
182
+ };
183
+
184
+ view.dispatch({ effects: openPopover.of(req) });
185
+ return true;
186
+ },
187
+ },
188
+ ]),
189
+ codeBlockLineNumbers,
190
+ codeBlockTheme,
191
+ ];
192
+ }
193
+
194
+ // Re-export for convenience
195
+ export { applyCodeBlock, findCodeFenceAtCursor };
@@ -0,0 +1,95 @@
1
+ import type { Extension } from "@codemirror/state";
2
+ import { keymap } from "@codemirror/view";
3
+ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
4
+ import { tags } from "@lezer/highlight";
5
+ import { markRegistry, shortcutRegistry } from "../registry";
6
+ import { toggleHeading } from "../commands";
7
+
8
+ export interface HeadingPluginOptions {
9
+ shortcuts?: {
10
+ heading1?: string;
11
+ heading2?: string;
12
+ heading3?: string;
13
+ };
14
+ }
15
+
16
+ const defaultShortcuts = {
17
+ heading1: "Mod-Alt-1",
18
+ heading2: "Mod-Alt-2",
19
+ heading3: "Mod-Alt-3",
20
+ };
21
+
22
+ const headingHighlight = HighlightStyle.define([
23
+ {
24
+ tag: tags.heading1,
25
+ fontSize: "2em",
26
+ fontWeight: "var(--lp-font-weight-bold, 700)",
27
+ fontFamily: "var(--lp-font-display)",
28
+ },
29
+ {
30
+ tag: tags.heading2,
31
+ fontSize: "1.6em",
32
+ fontWeight: "var(--lp-font-weight-bold, 700)",
33
+ fontFamily: "var(--lp-font-display)",
34
+ },
35
+ {
36
+ tag: tags.heading3,
37
+ fontSize: "1.3em",
38
+ fontWeight: "var(--lp-font-weight-semibold, 600)",
39
+ fontFamily: "var(--lp-font-display)",
40
+ },
41
+ {
42
+ tag: tags.heading4,
43
+ fontSize: "1.1em",
44
+ fontWeight: "var(--lp-font-weight-semibold, 600)",
45
+ fontFamily: "var(--lp-font-display)",
46
+ },
47
+ {
48
+ tag: tags.heading5,
49
+ fontSize: "1.05em",
50
+ fontWeight: "var(--lp-font-weight-semibold, 600)",
51
+ fontFamily: "var(--lp-font-display)",
52
+ },
53
+ {
54
+ tag: tags.heading6,
55
+ fontSize: "1em",
56
+ fontWeight: "var(--lp-font-weight-semibold, 600)",
57
+ fontFamily: "var(--lp-font-display)",
58
+ },
59
+ ]);
60
+
61
+ export function headingPlugin(options?: HeadingPluginOptions): Extension {
62
+ const keys = { ...defaultShortcuts, ...options?.shortcuts };
63
+
64
+ const extensions: Extension[] = [
65
+ syntaxHighlighting(headingHighlight),
66
+ markRegistry.of([
67
+ { mark: "heading1", detect: (nodeName) => nodeName === "ATXHeading1" },
68
+ { mark: "heading2", detect: (nodeName) => nodeName === "ATXHeading2" },
69
+ { mark: "heading3", detect: (nodeName) => nodeName === "ATXHeading3" },
70
+ ]),
71
+ ];
72
+
73
+ const keybindings = [];
74
+ const shortcuts = [];
75
+
76
+ if (keys.heading1) {
77
+ keybindings.push({ key: keys.heading1, run: toggleHeading(1) });
78
+ shortcuts.push({ key: keys.heading1, action: "heading1", plugin: "heading" });
79
+ }
80
+ if (keys.heading2) {
81
+ keybindings.push({ key: keys.heading2, run: toggleHeading(2) });
82
+ shortcuts.push({ key: keys.heading2, action: "heading2", plugin: "heading" });
83
+ }
84
+ if (keys.heading3) {
85
+ keybindings.push({ key: keys.heading3, run: toggleHeading(3) });
86
+ shortcuts.push({ key: keys.heading3, action: "heading3", plugin: "heading" });
87
+ }
88
+
89
+ if (keybindings.length > 0) {
90
+ extensions.push(keymap.of(keybindings));
91
+ extensions.push(shortcutRegistry.of(shortcuts));
92
+ }
93
+
94
+ return extensions;
95
+ }
@@ -0,0 +1,16 @@
1
+ export { inlinePlugin } from "./inline";
2
+ export { headingPlugin } from "./heading";
3
+ export { listPlugin } from "./list";
4
+ export { blockquotePlugin } from "./blockquote";
5
+ export { linkPlugin } from "./link";
6
+ export { codeBlockPlugin } from "./code-block";
7
+ export { tablePlugin } from "./table";
8
+ export { bracketPlugin } from "./bracket";
9
+
10
+ export type { InlinePluginOptions } from "./inline";
11
+ export type { HeadingPluginOptions } from "./heading";
12
+ export type { ListPluginOptions } from "./list";
13
+ export type { BlockquotePluginOptions } from "./blockquote";
14
+ export type { LinkPluginOptions, LinkData, LinkRequest } from "./link";
15
+ export type { CodeBlockPluginOptions, CodeBlockData, CodeBlockRequest } from "./code-block";
16
+ export type { TablePluginOptions } from "./table";
@@ -0,0 +1,62 @@
1
+ import type { Extension } from "@codemirror/state";
2
+ import { keymap } from "@codemirror/view";
3
+ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
4
+ import { tags } from "@lezer/highlight";
5
+ import { shortcutRegistry, markRegistry } from "../registry";
6
+ import { toggleMarker } from "../commands";
7
+
8
+ export interface InlinePluginOptions {
9
+ shortcuts?: {
10
+ bold?: string;
11
+ italic?: string;
12
+ strikethrough?: string;
13
+ code?: string;
14
+ };
15
+ }
16
+
17
+ const defaultShortcuts = {
18
+ bold: "Mod-b",
19
+ italic: "Mod-i",
20
+ strikethrough: "Mod-Shift-x",
21
+ code: "Mod-e",
22
+ };
23
+
24
+ const inlineHighlight = HighlightStyle.define([
25
+ { tag: tags.strong, fontWeight: "var(--lp-font-weight-bold, 700)" },
26
+ { tag: tags.emphasis, fontStyle: "italic" },
27
+ { tag: tags.strikethrough, textDecoration: "line-through" },
28
+ {
29
+ tag: tags.monospace,
30
+ fontFamily: "var(--lp-font-mono)",
31
+ fontSize: "0.875em",
32
+ backgroundColor: "var(--lp-color-surface)",
33
+ borderRadius: "var(--lp-radius, 6px)",
34
+ padding: "0.1em 0.3em",
35
+ },
36
+ ]);
37
+
38
+ export function inlinePlugin(options?: InlinePluginOptions): Extension {
39
+ const keys = { ...defaultShortcuts, ...options?.shortcuts };
40
+
41
+ return [
42
+ syntaxHighlighting(inlineHighlight),
43
+ shortcutRegistry.of([
44
+ { key: keys.bold, action: "bold", plugin: "inline" },
45
+ { key: keys.italic, action: "italic", plugin: "inline" },
46
+ { key: keys.strikethrough, action: "strikethrough", plugin: "inline" },
47
+ { key: keys.code, action: "code", plugin: "inline" },
48
+ ]),
49
+ markRegistry.of([
50
+ { mark: "bold", detect: (nodeName) => nodeName === "StrongEmphasis" },
51
+ { mark: "italic", detect: (nodeName) => nodeName === "Emphasis" },
52
+ { mark: "strikethrough", detect: (nodeName) => nodeName === "Strikethrough" },
53
+ { mark: "code", detect: (nodeName) => nodeName === "InlineCode" },
54
+ ]),
55
+ keymap.of([
56
+ { key: keys.bold, run: toggleMarker("**") },
57
+ { key: keys.italic, run: toggleMarker("_") },
58
+ { key: keys.strikethrough, run: toggleMarker("~~") },
59
+ { key: keys.code, run: toggleMarker("`") },
60
+ ]),
61
+ ];
62
+ }