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.
- package/dist/core/active-marks.d.ts +15 -0
- package/dist/core/active-marks.js +57 -0
- package/dist/core/commands.d.ts +39 -0
- package/dist/core/commands.js +415 -0
- package/dist/core/editor.d.ts +25 -0
- package/dist/core/editor.js +102 -0
- package/dist/core/field-notifier.d.ts +21 -0
- package/dist/core/field-notifier.js +56 -0
- package/dist/core/highlight-style.js +60 -0
- package/dist/core/highlight.d.ts +8 -0
- package/dist/core/highlight.js +34 -0
- package/dist/core/plugins/blockquote.d.ts +11 -0
- package/dist/core/plugins/blockquote.js +78 -0
- package/dist/core/plugins/bracket.d.ts +6 -0
- package/dist/core/plugins/bracket.js +38 -0
- package/dist/core/plugins/code-block.d.ts +27 -0
- package/dist/core/plugins/code-block.js +207 -0
- package/dist/core/plugins/heading.d.ts +13 -0
- package/dist/core/plugins/heading.js +111 -0
- package/dist/core/plugins/inline.d.ts +14 -0
- package/dist/core/plugins/inline.js +103 -0
- package/dist/core/plugins/link.d.ts +25 -0
- package/dist/core/plugins/link.js +104 -0
- package/dist/core/plugins/list.d.ts +14 -0
- package/dist/core/plugins/list.js +91 -0
- package/dist/core/plugins/table.d.ts +12 -0
- package/dist/core/plugins/table.js +161 -0
- package/dist/core/plugins.d.ts +9 -0
- package/dist/core/plugins.js +9 -0
- package/dist/core/popover.d.ts +9 -0
- package/dist/core/popover.js +16 -0
- package/dist/core/registry.d.ts +10 -0
- package/dist/core/registry.js +8 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.js +0 -0
- package/dist/core/utils.d.ts +13 -0
- package/dist/core/utils.js +32 -0
- package/dist/leepi.css +461 -0
- package/dist/react/code-block-popover.d.ts +76 -0
- package/dist/react/code-block-popover.js +223 -0
- package/dist/react/context.d.ts +42 -0
- package/dist/react/context.js +88 -0
- package/dist/react/editor.d.ts +30 -0
- package/dist/react/editor.js +60 -0
- package/dist/react/floating-toolbar.d.ts +30 -0
- package/dist/react/floating-toolbar.js +87 -0
- package/dist/react/link-popover.d.ts +70 -0
- package/dist/react/link-popover.js +222 -0
- package/dist/react/preview.d.ts +13 -0
- package/dist/react/preview.js +56 -0
- package/dist/react/toolbar.d.ts +51 -0
- package/dist/react/toolbar.js +161 -0
- package/package.json +90 -1
- package/src/core/active-marks.ts +89 -0
- package/src/core/commands.ts +461 -0
- package/src/core/editor.ts +139 -0
- package/src/core/field-notifier.ts +71 -0
- package/src/core/highlight-style.ts +66 -0
- package/src/core/highlight.ts +50 -0
- package/src/core/plugins/blockquote.ts +108 -0
- package/src/core/plugins/bracket.ts +34 -0
- package/src/core/plugins/code-block.ts +195 -0
- package/src/core/plugins/heading.ts +95 -0
- package/src/core/plugins/index.ts +16 -0
- package/src/core/plugins/inline.ts +62 -0
- package/src/core/plugins/link.ts +124 -0
- package/src/core/plugins/list.ts +68 -0
- package/src/core/plugins/table.ts +217 -0
- package/src/core/popover.ts +17 -0
- package/src/core/registry.ts +18 -0
- package/src/core/types.ts +25 -0
- package/src/core/utils.ts +38 -0
- package/src/react/code-block-popover.tsx +387 -0
- package/src/react/context.tsx +153 -0
- package/src/react/editor.tsx +106 -0
- package/src/react/floating-toolbar.tsx +161 -0
- package/src/react/link-popover.tsx +354 -0
- package/src/react/preview.tsx +80 -0
- package/src/react/toolbar.tsx +294 -0
- package/src/styles/floating-toolbar.css +52 -0
- package/src/styles/leepi.css +2 -0
- package/src/styles/popover.css +93 -0
- package/src/styles/preview.css +191 -0
- package/src/styles/theme.css +99 -0
- package/src/styles/tokens.css +63 -0
- package/src/styles/toolbar.css +55 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { insertLink } from "../core/commands.js";
|
|
2
|
+
import { closePopover, popoverField } from "../core/popover.js";
|
|
3
|
+
import { useEditorContext, useStateField, useVirtualAnchor } from "./context.js";
|
|
4
|
+
import { EditorSelection } from "@codemirror/state";
|
|
5
|
+
import { createContext, useCallback, useContext, useId, useMemo, useState } from "react";
|
|
6
|
+
import { Popover } from "@base-ui/react/popover";
|
|
7
|
+
import { Form } from "@base-ui/react/form";
|
|
8
|
+
import { Field } from "@base-ui/react/field";
|
|
9
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
10
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
//#region src/react/link-popover.tsx
|
|
13
|
+
const LinkPopoverCtx = createContext(null);
|
|
14
|
+
function useLinkPopoverCtx() {
|
|
15
|
+
const ctx = useContext(LinkPopoverCtx);
|
|
16
|
+
if (!ctx) throw new Error("leepi: LinkPopover sub-components must be used inside LinkPopover.Root");
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
|
19
|
+
function LabelInput({ autoFocus, placeholder, render, children, ...restProps }) {
|
|
20
|
+
const { label, setLabel } = useLinkPopoverCtx();
|
|
21
|
+
const id = useId();
|
|
22
|
+
const element = useRender({
|
|
23
|
+
render,
|
|
24
|
+
defaultTagName: "input",
|
|
25
|
+
props: mergeProps({
|
|
26
|
+
id,
|
|
27
|
+
value: label,
|
|
28
|
+
onChange: (e) => setLabel(e.target.value),
|
|
29
|
+
autoFocus,
|
|
30
|
+
placeholder,
|
|
31
|
+
className: "lp-popover-input"
|
|
32
|
+
}, restProps)
|
|
33
|
+
});
|
|
34
|
+
return /* @__PURE__ */ jsxs(Field.Root, {
|
|
35
|
+
className: "lp-popover-field",
|
|
36
|
+
children: [/* @__PURE__ */ jsx(Field.Label, {
|
|
37
|
+
htmlFor: id,
|
|
38
|
+
className: "lp-popover-label",
|
|
39
|
+
children
|
|
40
|
+
}), element]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function UrlInput({ autoFocus, placeholder, render, children, ...restProps }) {
|
|
44
|
+
const { url, setUrl } = useLinkPopoverCtx();
|
|
45
|
+
const id = useId();
|
|
46
|
+
const element = useRender({
|
|
47
|
+
render,
|
|
48
|
+
defaultTagName: "input",
|
|
49
|
+
props: mergeProps({
|
|
50
|
+
id,
|
|
51
|
+
type: "url",
|
|
52
|
+
value: url,
|
|
53
|
+
onChange: (e) => setUrl(e.target.value),
|
|
54
|
+
autoFocus,
|
|
55
|
+
placeholder,
|
|
56
|
+
className: "lp-popover-input"
|
|
57
|
+
}, restProps)
|
|
58
|
+
});
|
|
59
|
+
return /* @__PURE__ */ jsxs(Field.Root, {
|
|
60
|
+
className: "lp-popover-field",
|
|
61
|
+
children: [/* @__PURE__ */ jsx(Field.Label, {
|
|
62
|
+
htmlFor: id,
|
|
63
|
+
className: "lp-popover-label",
|
|
64
|
+
children
|
|
65
|
+
}), element]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function Actions({ cancelLabel, submitLabel, removeLabel }) {
|
|
69
|
+
const { isEditing, url, onRemove, onClose } = useLinkPopoverCtx();
|
|
70
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
71
|
+
className: "lp-popover-actions",
|
|
72
|
+
children: [isEditing ? /* @__PURE__ */ jsx("button", {
|
|
73
|
+
type: "button",
|
|
74
|
+
onClick: onRemove,
|
|
75
|
+
className: "lp-popover-btn lp-popover-btn--danger",
|
|
76
|
+
children: removeLabel
|
|
77
|
+
}) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsxs("div", {
|
|
78
|
+
className: "lp-popover-actions-end",
|
|
79
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
80
|
+
type: "button",
|
|
81
|
+
onClick: onClose,
|
|
82
|
+
className: "lp-popover-btn lp-popover-btn--secondary",
|
|
83
|
+
children: cancelLabel
|
|
84
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
85
|
+
type: "submit",
|
|
86
|
+
disabled: !url.trim(),
|
|
87
|
+
className: "lp-popover-btn lp-popover-btn--primary",
|
|
88
|
+
children: submitLabel
|
|
89
|
+
})]
|
|
90
|
+
})]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function LinkPopoverContent({ initialLabel, initialUrl, onSubmit, onRemove, onClose, children }) {
|
|
94
|
+
const [label, setLabel] = useState(initialLabel || "");
|
|
95
|
+
const [url, setUrl] = useState(initialUrl || "");
|
|
96
|
+
const isEditing = initialUrl !== "";
|
|
97
|
+
const handleSubmit = useCallback((e) => {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
if (!url.trim()) return;
|
|
100
|
+
onSubmit(label || url, url);
|
|
101
|
+
}, [
|
|
102
|
+
label,
|
|
103
|
+
url,
|
|
104
|
+
onSubmit
|
|
105
|
+
]);
|
|
106
|
+
return /* @__PURE__ */ jsx(LinkPopoverCtx, {
|
|
107
|
+
value: useMemo(() => ({
|
|
108
|
+
label,
|
|
109
|
+
setLabel,
|
|
110
|
+
url,
|
|
111
|
+
setUrl,
|
|
112
|
+
isEditing,
|
|
113
|
+
onRemove,
|
|
114
|
+
onClose
|
|
115
|
+
}), [
|
|
116
|
+
label,
|
|
117
|
+
url,
|
|
118
|
+
isEditing,
|
|
119
|
+
onRemove,
|
|
120
|
+
onClose
|
|
121
|
+
]),
|
|
122
|
+
children: /* @__PURE__ */ jsx(Form, {
|
|
123
|
+
onSubmit: handleSubmit,
|
|
124
|
+
className: "lp-popover-form",
|
|
125
|
+
children
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function LinkPopoverRoot({ x, y, onClose, ...contentProps }) {
|
|
130
|
+
const virtualAnchor = useMemo(() => ({ getBoundingClientRect: () => ({
|
|
131
|
+
x,
|
|
132
|
+
y,
|
|
133
|
+
width: 0,
|
|
134
|
+
height: 0,
|
|
135
|
+
top: y,
|
|
136
|
+
left: x,
|
|
137
|
+
right: x,
|
|
138
|
+
bottom: y,
|
|
139
|
+
toJSON() {
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
}) }), [x, y]);
|
|
143
|
+
return /* @__PURE__ */ jsx(Popover.Root, {
|
|
144
|
+
open: true,
|
|
145
|
+
onOpenChange: (open) => {
|
|
146
|
+
if (!open) onClose();
|
|
147
|
+
},
|
|
148
|
+
children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
149
|
+
anchor: virtualAnchor,
|
|
150
|
+
positionMethod: "fixed",
|
|
151
|
+
side: "bottom",
|
|
152
|
+
align: "start",
|
|
153
|
+
sideOffset: 8,
|
|
154
|
+
className: "lp-popover-positioner",
|
|
155
|
+
children: /* @__PURE__ */ jsx(Popover.Popup, {
|
|
156
|
+
className: "lp-popover",
|
|
157
|
+
children: /* @__PURE__ */ jsx(LinkPopoverContent, {
|
|
158
|
+
onClose,
|
|
159
|
+
...contentProps
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
}) })
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function LinkPopoverConnected({ children }) {
|
|
166
|
+
const { view } = useEditorContext();
|
|
167
|
+
const popoverReq = useStateField(popoverField, null);
|
|
168
|
+
const linkReq = popoverReq?.type === "link" ? popoverReq : null;
|
|
169
|
+
const handleSubmit = useCallback((label, url) => {
|
|
170
|
+
if (!view || !linkReq) return;
|
|
171
|
+
const { from, to } = linkReq.data;
|
|
172
|
+
insertLink(view, from, to, label, url);
|
|
173
|
+
view.dispatch({ effects: closePopover.of() });
|
|
174
|
+
}, [view, linkReq]);
|
|
175
|
+
const handleRemove = useCallback(() => {
|
|
176
|
+
if (!view || !linkReq) return;
|
|
177
|
+
const { from, to, text } = linkReq.data;
|
|
178
|
+
view.dispatch({
|
|
179
|
+
changes: {
|
|
180
|
+
from,
|
|
181
|
+
to,
|
|
182
|
+
insert: text
|
|
183
|
+
},
|
|
184
|
+
selection: EditorSelection.cursor(from + text.length)
|
|
185
|
+
});
|
|
186
|
+
view.dispatch({ effects: closePopover.of() });
|
|
187
|
+
view.focus();
|
|
188
|
+
}, [view, linkReq]);
|
|
189
|
+
const handleClose = useCallback(() => {
|
|
190
|
+
if (!view) return;
|
|
191
|
+
view.dispatch({ effects: closePopover.of() });
|
|
192
|
+
view.focus();
|
|
193
|
+
}, [view]);
|
|
194
|
+
const virtualAnchor = useVirtualAnchor(linkReq?.data.from ?? null);
|
|
195
|
+
return /* @__PURE__ */ jsx(Popover.Root, {
|
|
196
|
+
open: !!linkReq,
|
|
197
|
+
onOpenChange: (open) => {
|
|
198
|
+
if (!open) handleClose();
|
|
199
|
+
},
|
|
200
|
+
children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
201
|
+
anchor: virtualAnchor,
|
|
202
|
+
positionMethod: "fixed",
|
|
203
|
+
side: "bottom",
|
|
204
|
+
align: "start",
|
|
205
|
+
sideOffset: 8,
|
|
206
|
+
className: "lp-popover-positioner",
|
|
207
|
+
children: /* @__PURE__ */ jsx(Popover.Popup, {
|
|
208
|
+
className: "lp-popover",
|
|
209
|
+
children: linkReq && /* @__PURE__ */ jsx(LinkPopoverContent, {
|
|
210
|
+
initialLabel: linkReq.data.text,
|
|
211
|
+
initialUrl: linkReq.data.url,
|
|
212
|
+
onSubmit: handleSubmit,
|
|
213
|
+
onRemove: handleRemove,
|
|
214
|
+
onClose: handleClose,
|
|
215
|
+
children
|
|
216
|
+
}, `${linkReq.data.from}-${linkReq.data.to}`)
|
|
217
|
+
})
|
|
218
|
+
}) })
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
export { Actions, LinkPopoverContent as Content, LabelInput, LinkPopoverConnected, LinkPopoverRoot as Root, UrlInput };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { JSX } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/react/preview.d.ts
|
|
4
|
+
interface PreviewProps {
|
|
5
|
+
value: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
declare function Preview({
|
|
9
|
+
value,
|
|
10
|
+
className
|
|
11
|
+
}: PreviewProps): JSX.Element;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { Preview, PreviewProps };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { highlightCode } from "../core/highlight.js";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { Marked } from "marked";
|
|
5
|
+
import { markedHighlight } from "marked-highlight";
|
|
6
|
+
//#region src/react/preview.tsx
|
|
7
|
+
function parseFenceInfo(info) {
|
|
8
|
+
const parts = info.trim().split(/\s+/);
|
|
9
|
+
let lang = "";
|
|
10
|
+
let filename = "";
|
|
11
|
+
for (const part of parts) if (part.startsWith("filename=")) filename = part.slice(9);
|
|
12
|
+
else if (!lang) lang = part;
|
|
13
|
+
return {
|
|
14
|
+
lang,
|
|
15
|
+
filename
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const marked = new Marked({ gfm: true }, markedHighlight({
|
|
19
|
+
async: true,
|
|
20
|
+
langPrefix: "language-",
|
|
21
|
+
async highlight(code, lang) {
|
|
22
|
+
const actualLang = lang.split(/\s+/)[0];
|
|
23
|
+
if (!actualLang) return code;
|
|
24
|
+
return highlightCode(code, actualLang);
|
|
25
|
+
}
|
|
26
|
+
}), { renderer: { code({ text, lang: rawLang }) {
|
|
27
|
+
const { lang, filename } = parseFenceInfo(rawLang || "");
|
|
28
|
+
let header = "";
|
|
29
|
+
if (filename || lang) header = `<div class="lp-code-header"><span>${filename || lang}</span>${filename && lang ? `<span class="lp-code-header-lang">${lang}</span>` : ""}</div>`;
|
|
30
|
+
const lines = text.split("\n");
|
|
31
|
+
if (lines.at(-1) === "") lines.pop();
|
|
32
|
+
const rows = lines.map((line, i) => `<span class="lp-code-row"><span class="lp-code-line-number">${i + 1}</span><span class="lp-code-line">${line || " "}</span></span>`).join("");
|
|
33
|
+
return `<div class="lp-code-block">${header}<pre><code class="language-${lang}">${rows}</code></pre></div>`;
|
|
34
|
+
} } });
|
|
35
|
+
function Preview({ value, className }) {
|
|
36
|
+
const [html, setHtml] = useState("");
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!value) {
|
|
39
|
+
setHtml("");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let cancelled = false;
|
|
43
|
+
marked.parse(value).then((result) => {
|
|
44
|
+
if (!cancelled) setHtml(result);
|
|
45
|
+
});
|
|
46
|
+
return () => {
|
|
47
|
+
cancelled = true;
|
|
48
|
+
};
|
|
49
|
+
}, [value]);
|
|
50
|
+
return /* @__PURE__ */ jsx("div", {
|
|
51
|
+
className: `lp-preview ${className || ""}`,
|
|
52
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { Preview };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ComponentProps, JSX, ReactNode } from "react";
|
|
2
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
3
|
+
|
|
4
|
+
//#region src/react/toolbar.d.ts
|
|
5
|
+
type InlineAction = "bold" | "italic" | "strikethrough" | "code" | "link";
|
|
6
|
+
type BlockAction = "heading1" | "heading2" | "heading3" | "orderedList" | "unorderedList" | "taskList" | "blockquote" | "codeblock";
|
|
7
|
+
interface ToolbarInlineButtonProps extends Omit<ComponentProps<typeof Toolbar.Button>, "children"> {
|
|
8
|
+
action: InlineAction;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
declare function ToolbarInlineButton({
|
|
12
|
+
action,
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}: ToolbarInlineButtonProps): JSX.Element;
|
|
17
|
+
interface ToolbarBlockButtonProps extends Omit<ComponentProps<typeof Toolbar.Button>, "children"> {
|
|
18
|
+
action: BlockAction;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
declare function ToolbarBlockButton({
|
|
22
|
+
action,
|
|
23
|
+
children,
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: ToolbarBlockButtonProps): JSX.Element;
|
|
27
|
+
type ToolbarButtonProps = ComponentProps<typeof Toolbar.Button>;
|
|
28
|
+
declare function ToolbarButton({
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}: ToolbarButtonProps): JSX.Element;
|
|
32
|
+
type ToolbarSeparatorProps = ComponentProps<typeof Toolbar.Separator>;
|
|
33
|
+
declare function ToolbarSeparator({
|
|
34
|
+
className,
|
|
35
|
+
...props
|
|
36
|
+
}: ToolbarSeparatorProps): JSX.Element;
|
|
37
|
+
type ToolbarGroupProps = ComponentProps<typeof Toolbar.Group>;
|
|
38
|
+
declare function ToolbarGroup({
|
|
39
|
+
className,
|
|
40
|
+
...props
|
|
41
|
+
}: ToolbarGroupProps): JSX.Element;
|
|
42
|
+
interface ToolbarProps extends Omit<ComponentProps<typeof Toolbar.Root>, "children"> {
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
declare function ToolbarRoot({
|
|
46
|
+
children,
|
|
47
|
+
className,
|
|
48
|
+
...props
|
|
49
|
+
}: ToolbarProps): JSX.Element;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { BlockAction, ToolbarBlockButton as BlockButton, ToolbarButton as Button, ToolbarGroup as Group, InlineAction, ToolbarInlineButton as InlineButton, ToolbarRoot as Root, ToolbarSeparator as Separator, ToolbarBlockButtonProps, ToolbarInlineButtonProps, ToolbarProps };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { findCodeFenceAtCursor, findLinkAtCursor, toggleBlockquote, toggleHeading, toggleListKind, toggleMarker } from "../core/commands.js";
|
|
2
|
+
import { openPopover } from "../core/popover.js";
|
|
3
|
+
import { useActiveMarks, useEditorContext } from "./context.js";
|
|
4
|
+
import { useCallback } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
7
|
+
//#region src/react/toolbar.tsx
|
|
8
|
+
const inlineCommands = {
|
|
9
|
+
bold: toggleMarker("**"),
|
|
10
|
+
italic: toggleMarker("_"),
|
|
11
|
+
strikethrough: toggleMarker("~~"),
|
|
12
|
+
code: toggleMarker("`")
|
|
13
|
+
};
|
|
14
|
+
const blockCommands = {
|
|
15
|
+
heading1: toggleHeading(1),
|
|
16
|
+
heading2: toggleHeading(2),
|
|
17
|
+
heading3: toggleHeading(3),
|
|
18
|
+
orderedList: toggleListKind("ul"),
|
|
19
|
+
unorderedList: toggleListKind("ol"),
|
|
20
|
+
taskList: toggleListKind("task"),
|
|
21
|
+
blockquote: toggleBlockquote
|
|
22
|
+
};
|
|
23
|
+
function preventFocusLoss(e) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
}
|
|
26
|
+
function ToolbarInlineButton({ action, children, className, ...props }) {
|
|
27
|
+
const { view } = useEditorContext();
|
|
28
|
+
const marks = useActiveMarks();
|
|
29
|
+
const active = marks[action] ?? false;
|
|
30
|
+
const disabled = marks.codeblock ?? false;
|
|
31
|
+
const handleClick = useCallback(() => {
|
|
32
|
+
if (!view || disabled) return;
|
|
33
|
+
if (action === "link") {
|
|
34
|
+
const { state } = view;
|
|
35
|
+
const range = state.selection.main;
|
|
36
|
+
const existing = findLinkAtCursor(state, range.from);
|
|
37
|
+
const coords = view.coordsAtPos(range.from);
|
|
38
|
+
if (!coords) return;
|
|
39
|
+
const req = existing ? {
|
|
40
|
+
type: "link",
|
|
41
|
+
x: coords.left,
|
|
42
|
+
y: coords.bottom,
|
|
43
|
+
data: {
|
|
44
|
+
text: existing.label,
|
|
45
|
+
url: existing.url,
|
|
46
|
+
from: existing.from,
|
|
47
|
+
to: existing.to
|
|
48
|
+
}
|
|
49
|
+
} : {
|
|
50
|
+
type: "link",
|
|
51
|
+
x: coords.left,
|
|
52
|
+
y: coords.bottom,
|
|
53
|
+
data: {
|
|
54
|
+
text: state.sliceDoc(range.from, range.to),
|
|
55
|
+
url: "",
|
|
56
|
+
from: range.from,
|
|
57
|
+
to: range.to
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
view.dispatch({ effects: openPopover.of(req) });
|
|
61
|
+
} else {
|
|
62
|
+
inlineCommands[action](view);
|
|
63
|
+
view.focus();
|
|
64
|
+
}
|
|
65
|
+
}, [
|
|
66
|
+
view,
|
|
67
|
+
disabled,
|
|
68
|
+
action
|
|
69
|
+
]);
|
|
70
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
71
|
+
disabled,
|
|
72
|
+
className: `lp-toolbar-btn${active ? " lp-toolbar-btn--active" : ""} ${className || ""}`,
|
|
73
|
+
onMouseDown: preventFocusLoss,
|
|
74
|
+
onClick: handleClick,
|
|
75
|
+
...props,
|
|
76
|
+
children
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function ToolbarBlockButton({ action, children, className, ...props }) {
|
|
80
|
+
const { view } = useEditorContext();
|
|
81
|
+
const marks = useActiveMarks();
|
|
82
|
+
const active = marks[action] ?? false;
|
|
83
|
+
const disabled = (marks.codeblock ?? false) && action !== "codeblock";
|
|
84
|
+
const handleClick = useCallback(() => {
|
|
85
|
+
if (!view || disabled) return;
|
|
86
|
+
if (action === "codeblock") {
|
|
87
|
+
const { state } = view;
|
|
88
|
+
const range = state.selection.main;
|
|
89
|
+
const coords = view.coordsAtPos(range.from);
|
|
90
|
+
if (!coords) return;
|
|
91
|
+
const existing = findCodeFenceAtCursor(view);
|
|
92
|
+
const req = existing ? {
|
|
93
|
+
type: "codeblock",
|
|
94
|
+
x: coords.left,
|
|
95
|
+
y: coords.bottom,
|
|
96
|
+
data: {
|
|
97
|
+
lang: existing.lang,
|
|
98
|
+
filename: existing.filename,
|
|
99
|
+
fenceFrom: existing.fenceLine.from,
|
|
100
|
+
fenceTo: existing.fenceLine.to,
|
|
101
|
+
isNew: false
|
|
102
|
+
}
|
|
103
|
+
} : {
|
|
104
|
+
type: "codeblock",
|
|
105
|
+
x: coords.left,
|
|
106
|
+
y: coords.bottom,
|
|
107
|
+
data: {
|
|
108
|
+
lang: "",
|
|
109
|
+
filename: "",
|
|
110
|
+
fenceFrom: 0,
|
|
111
|
+
fenceTo: 0,
|
|
112
|
+
isNew: true,
|
|
113
|
+
insertPos: range.from
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
view.dispatch({ effects: openPopover.of(req) });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
blockCommands[action](view);
|
|
120
|
+
view.focus();
|
|
121
|
+
}, [
|
|
122
|
+
view,
|
|
123
|
+
disabled,
|
|
124
|
+
action
|
|
125
|
+
]);
|
|
126
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
127
|
+
disabled,
|
|
128
|
+
className: `lp-toolbar-btn${active ? " lp-toolbar-btn--active" : ""} ${className || ""}`,
|
|
129
|
+
onMouseDown: preventFocusLoss,
|
|
130
|
+
onClick: handleClick,
|
|
131
|
+
...props,
|
|
132
|
+
children
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function ToolbarButton({ className, ...props }) {
|
|
136
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
137
|
+
className: `lp-toolbar-btn ${className || ""}`,
|
|
138
|
+
...props
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function ToolbarSeparator({ className, ...props }) {
|
|
142
|
+
return /* @__PURE__ */ jsx(Toolbar.Separator, {
|
|
143
|
+
className: `lp-toolbar-separator ${className || ""}`,
|
|
144
|
+
...props
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function ToolbarGroup({ className, ...props }) {
|
|
148
|
+
return /* @__PURE__ */ jsx(Toolbar.Group, {
|
|
149
|
+
className: `lp-toolbar-group ${className || ""}`,
|
|
150
|
+
...props
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function ToolbarRoot({ children, className, ...props }) {
|
|
154
|
+
return /* @__PURE__ */ jsx(Toolbar.Root, {
|
|
155
|
+
className: `lp-toolbar ${className || ""}`,
|
|
156
|
+
...props,
|
|
157
|
+
children
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { ToolbarBlockButton as BlockButton, ToolbarButton as Button, ToolbarGroup as Group, ToolbarInlineButton as InlineButton, ToolbarRoot as Root, ToolbarSeparator as Separator };
|
package/package.json
CHANGED
|
@@ -1 +1,90 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "leepi",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "A composable React markdown editor with inline styling",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/brijbyte/leepi.git",
|
|
8
|
+
"directory": "packages/leepi"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"!src/**/*.test.ts",
|
|
14
|
+
"!src/**/test-helpers.ts"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
"./core/active-marks": "./dist/core/active-marks.js",
|
|
19
|
+
"./core/commands": "./dist/core/commands.js",
|
|
20
|
+
"./core/editor": "./dist/core/editor.js",
|
|
21
|
+
"./core/field-notifier": "./dist/core/field-notifier.js",
|
|
22
|
+
"./core/highlight": "./dist/core/highlight.js",
|
|
23
|
+
"./core/plugins": "./dist/core/plugins.js",
|
|
24
|
+
"./core/popover": "./dist/core/popover.js",
|
|
25
|
+
"./core/registry": "./dist/core/registry.js",
|
|
26
|
+
"./core/types": "./dist/core/types.js",
|
|
27
|
+
"./core/utils": "./dist/core/utils.js",
|
|
28
|
+
"./react/code-block-popover": "./dist/react/code-block-popover.js",
|
|
29
|
+
"./react/context": "./dist/react/context.js",
|
|
30
|
+
"./react/editor": "./dist/react/editor.js",
|
|
31
|
+
"./react/floating-toolbar": "./dist/react/floating-toolbar.js",
|
|
32
|
+
"./react/link-popover": "./dist/react/link-popover.js",
|
|
33
|
+
"./react/preview": "./dist/react/preview.js",
|
|
34
|
+
"./react/toolbar": "./dist/react/toolbar.js",
|
|
35
|
+
"./package.json": "./package.json",
|
|
36
|
+
"./styles.css": "./dist/leepi.css",
|
|
37
|
+
"./styles/*.css": "./src/styles/*.css"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"provenance": true
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@codemirror/commands": "^6.0.0",
|
|
45
|
+
"@codemirror/lang-markdown": "^6.0.0",
|
|
46
|
+
"@codemirror/language": "^6.0.0",
|
|
47
|
+
"@codemirror/language-data": "^6.0.0",
|
|
48
|
+
"@codemirror/state": "^6.0.0",
|
|
49
|
+
"@codemirror/view": "^6.0.0",
|
|
50
|
+
"@lezer/highlight": "^1.0.0",
|
|
51
|
+
"marked": ">=15.0.0",
|
|
52
|
+
"marked-highlight": "^2.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@base-ui/react": "^1.3.0",
|
|
56
|
+
"@codemirror/commands": "^6.10.3",
|
|
57
|
+
"@codemirror/lang-markdown": "^6.5.0",
|
|
58
|
+
"@codemirror/language": "^6.12.3",
|
|
59
|
+
"@codemirror/language-data": "^6.5.2",
|
|
60
|
+
"@codemirror/state": "^6.6.0",
|
|
61
|
+
"@codemirror/view": "^6.41.0",
|
|
62
|
+
"@lezer/highlight": "^1.2.3",
|
|
63
|
+
"@types/react": "^19.2.14",
|
|
64
|
+
"@types/react-dom": "^19.2.3",
|
|
65
|
+
"marked": ">=18.0.0",
|
|
66
|
+
"marked-highlight": "^2.2.3",
|
|
67
|
+
"react": "^19.2.4",
|
|
68
|
+
"react-dom": "^19.2.4"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"@base-ui/react": "^1.0.0",
|
|
72
|
+
"react": "^19.0.0",
|
|
73
|
+
"react-dom": "^19.0.0"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"@base-ui/react": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"react": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"react-dom": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"scripts": {
|
|
87
|
+
"build": "tsdown",
|
|
88
|
+
"typecheck": "tsc --noEmit"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type Extension } from "@codemirror/state";
|
|
2
|
+
import { type EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
|
|
3
|
+
import { syntaxTree } from "@codemirror/language";
|
|
4
|
+
import { markRegistry } from "./registry";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Active marks as a dynamic record. Keys are mark names registered via `markRegistry`.
|
|
8
|
+
*/
|
|
9
|
+
export type ActiveMarks = Record<string, boolean>;
|
|
10
|
+
|
|
11
|
+
export const emptyMarks: ActiveMarks = {};
|
|
12
|
+
|
|
13
|
+
export function detectMarks(view: EditorView): ActiveMarks {
|
|
14
|
+
const { state } = view;
|
|
15
|
+
const pos = state.selection.main.from;
|
|
16
|
+
const tree = syntaxTree(state);
|
|
17
|
+
const detectors = state.facet(markRegistry);
|
|
18
|
+
|
|
19
|
+
const marks: ActiveMarks = {};
|
|
20
|
+
|
|
21
|
+
// Tree walk: call all detectors with each node name
|
|
22
|
+
let node = tree.resolveInner(pos, -1);
|
|
23
|
+
while (node) {
|
|
24
|
+
for (const det of detectors) {
|
|
25
|
+
if (!marks[det.mark] && det.detect(node.name, state, pos)) {
|
|
26
|
+
marks[det.mark] = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!node.parent) break;
|
|
30
|
+
node = node.parent;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Line pass: call detectors with "__line__" sentinel for line-level checks
|
|
34
|
+
for (const det of detectors) {
|
|
35
|
+
if (!marks[det.mark] && det.detect("__line__", state, pos)) {
|
|
36
|
+
marks[det.mark] = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return marks;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Notification system: views notify React subscribers when marks change
|
|
44
|
+
const viewListeners = new WeakMap<EditorView, Set<() => void>>();
|
|
45
|
+
const viewSnapshots = new WeakMap<EditorView, ActiveMarks>();
|
|
46
|
+
|
|
47
|
+
export function subscribeToMarks(view: EditorView, callback: () => void): () => void {
|
|
48
|
+
let listeners = viewListeners.get(view);
|
|
49
|
+
if (!listeners) {
|
|
50
|
+
listeners = new Set();
|
|
51
|
+
viewListeners.set(view, listeners);
|
|
52
|
+
}
|
|
53
|
+
listeners.add(callback);
|
|
54
|
+
return () => {
|
|
55
|
+
listeners!.delete(callback);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getMarksSnapshot(view: EditorView): ActiveMarks {
|
|
60
|
+
return viewSnapshots.get(view) ?? emptyMarks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function marksEqual(a: ActiveMarks, b: ActiveMarks): boolean {
|
|
64
|
+
const aKeys = Object.keys(a);
|
|
65
|
+
const bKeys = Object.keys(b);
|
|
66
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
67
|
+
for (const key of aKeys) {
|
|
68
|
+
if (a[key] !== b[key]) return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const activeMarksPlugin: Extension = ViewPlugin.fromClass(
|
|
74
|
+
class {
|
|
75
|
+
update(update: ViewUpdate): void {
|
|
76
|
+
if (update.selectionSet || update.docChanged) {
|
|
77
|
+
const marks = detectMarks(update.view);
|
|
78
|
+
const prev = viewSnapshots.get(update.view) ?? emptyMarks;
|
|
79
|
+
if (!marksEqual(prev, marks)) {
|
|
80
|
+
viewSnapshots.set(update.view, marks);
|
|
81
|
+
const listeners = viewListeners.get(update.view);
|
|
82
|
+
if (listeners) {
|
|
83
|
+
for (const cb of listeners) cb();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
);
|