md-editor-lite 0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,371 @@
1
+ // src/components/EditorHeader.tsx
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ function EditorHeader({
4
+ mode,
5
+ setMode,
6
+ insertMarkdown
7
+ }) {
8
+ return /* @__PURE__ */ jsxs("header", { className: "editor-header", children: [
9
+ /* @__PURE__ */ jsx(EditorTab, { mode, setMode }),
10
+ mode === "write" && /* @__PURE__ */ jsx(Toolbar, { insertMarkdown })
11
+ ] });
12
+ }
13
+
14
+ // src/components/EditorTab.tsx
15
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
16
+ function EditorTab({ mode, setMode }) {
17
+ return /* @__PURE__ */ jsxs2("div", { className: "editor-tab-container", children: [
18
+ /* @__PURE__ */ jsx2(
19
+ "div",
20
+ {
21
+ className: `editor-tab ${mode === "write" ? "editor-tab--active" : ""}`,
22
+ onClick: () => setMode("write"),
23
+ children: "Write"
24
+ }
25
+ ),
26
+ /* @__PURE__ */ jsx2(
27
+ "div",
28
+ {
29
+ className: `editor-tab ${mode === "preview" ? "editor-tab--active" : ""}`,
30
+ onClick: () => setMode("preview"),
31
+ children: "Preview"
32
+ }
33
+ )
34
+ ] });
35
+ }
36
+
37
+ // src/components/EditorTextarea.tsx
38
+ import { forwardRef } from "react";
39
+ import { jsx as jsx3 } from "react/jsx-runtime";
40
+ var EditorTextarea = forwardRef(({ value, placeholder, setValue, onImageUpload }, ref) => {
41
+ const handleDrop = async (e) => {
42
+ e.preventDefault();
43
+ if (!onImageUpload) return;
44
+ const files = Array.from(e.dataTransfer.files).filter(
45
+ (file) => file.type.startsWith("image/")
46
+ );
47
+ for (const file of files) {
48
+ const url = await onImageUpload(file);
49
+ setValue(value + `
50
+ ![${file.name}](${url})
51
+ `);
52
+ }
53
+ };
54
+ return /* @__PURE__ */ jsx3(
55
+ "textarea",
56
+ {
57
+ ref,
58
+ value,
59
+ onChange: (e) => setValue(e.target.value),
60
+ onDragOver: (e) => e.preventDefault(),
61
+ onDrop: handleDrop,
62
+ placeholder,
63
+ className: "editor-textarea"
64
+ }
65
+ );
66
+ });
67
+
68
+ // src/components/MarkdownEditor.tsx
69
+ import { useState } from "react";
70
+
71
+ // src/hooks/useMarkdownEditor.ts
72
+ import { useCallback, useRef } from "react";
73
+ function useMarkdownEditor({ value, setValue }) {
74
+ const textareaRef = useRef(null);
75
+ const insertMarkdown = useCallback(
76
+ (before, after = "") => {
77
+ const textarea = textareaRef.current;
78
+ if (!textarea) return;
79
+ const start = textarea.selectionStart;
80
+ const end = textarea.selectionEnd;
81
+ const selected = value.slice(start, end);
82
+ const replaced = before + (selected || "") + after;
83
+ setValue(value.slice(0, start) + replaced + value.slice(end));
84
+ requestAnimationFrame(() => {
85
+ if (!textareaRef.current) return;
86
+ const next = start + replaced.length;
87
+ textareaRef.current.focus();
88
+ textareaRef.current.selectionStart = next;
89
+ textareaRef.current.selectionEnd = next;
90
+ });
91
+ },
92
+ [value, setValue]
93
+ );
94
+ return {
95
+ textareaRef,
96
+ insertMarkdown
97
+ };
98
+ }
99
+
100
+ // src/components/MarkdownEditor.tsx
101
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
102
+ function MarkdownEditor({
103
+ value,
104
+ onChange: setValue,
105
+ onImageUpload
106
+ }) {
107
+ const { textareaRef, insertMarkdown } = useMarkdownEditor({
108
+ value,
109
+ setValue
110
+ });
111
+ const [mode, setMode] = useState("write");
112
+ return /* @__PURE__ */ jsxs3("div", { className: "editor", children: [
113
+ /* @__PURE__ */ jsx4(
114
+ EditorHeader,
115
+ {
116
+ mode,
117
+ setMode,
118
+ insertMarkdown
119
+ }
120
+ ),
121
+ mode === "write" ? /* @__PURE__ */ jsx4(
122
+ EditorTextarea,
123
+ {
124
+ ref: textareaRef,
125
+ value,
126
+ setValue,
127
+ onImageUpload
128
+ }
129
+ ) : /* @__PURE__ */ jsx4(Preview, { value })
130
+ ] });
131
+ }
132
+
133
+ // src/utils/markdown.ts
134
+ function parseBold(text) {
135
+ return text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
136
+ }
137
+ function parseItalic(text) {
138
+ return text.replace(/\*(.+?)\*/g, "<em>$1</em>");
139
+ }
140
+ function parseInlineCode(text) {
141
+ return text.replace(/`(.+?)`/g, "<code>$1</code>");
142
+ }
143
+ function parseBlockquote(text) {
144
+ return text.replace(/^> (.+)$/gm, "<blockquote>$1</blockquote>");
145
+ }
146
+ function parseImages(text) {
147
+ return text.replace(
148
+ /!\[(.*?)\]\(([^)]*?)\)/g,
149
+ (_, alt, url) => `<img src="${url}" alt="${alt || ""}" />`
150
+ );
151
+ }
152
+ function parseLinks(text) {
153
+ return text.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
154
+ }
155
+ function parseHeadings(text) {
156
+ return text.replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^### (.+)$/gm, "<h3>$1</h3>");
157
+ }
158
+ function parseUnorderedList(text) {
159
+ return text.replace(
160
+ /(^|\r?\n)((?:\s*- .+(?:\r?\n|$))+)/g,
161
+ (_, prefix, block) => {
162
+ const items = block.trim().split(/\r?\n/).map((line) => line.replace(/^\s*- /, "").trim()).filter(Boolean).map((item) => `<li>${item}</li>`).join("");
163
+ return `${prefix}<ul>${items}</ul>`;
164
+ }
165
+ );
166
+ }
167
+ function parseOrderedList(text) {
168
+ return text.replace(
169
+ /(^|\r?\n)((?:\d+\. .+(?:\r?\n|$))+)/g,
170
+ (_, prefix, block) => {
171
+ const items = block.trim().split(/\r?\n/).map((line) => line.replace(/^\d+\. /, "").trim()).filter(Boolean).map((item) => `<li>${item}</li>`).join("");
172
+ return `${prefix}<ol>${items}</ol>`;
173
+ }
174
+ );
175
+ }
176
+ function parseLineBreaks(text) {
177
+ return text.replace(
178
+ /(?<!<\/ul>|<\/ol>|<\/li>|<\/h1>|<\/h2>|<\/h3>)\n/g,
179
+ "<br/>"
180
+ );
181
+ }
182
+ function markdownToHtml(markdown) {
183
+ let html = markdown;
184
+ html = parseBold(html);
185
+ html = parseItalic(html);
186
+ html = parseInlineCode(html);
187
+ html = parseBlockquote(html);
188
+ html = parseImages(html);
189
+ html = parseLinks(html);
190
+ html = parseHeadings(html);
191
+ html = parseUnorderedList(html);
192
+ html = parseOrderedList(html);
193
+ html = parseLineBreaks(html);
194
+ return html;
195
+ }
196
+
197
+ // src/components/Preview.tsx
198
+ import { jsx as jsx5 } from "react/jsx-runtime";
199
+ function Preview({ value }) {
200
+ return /* @__PURE__ */ jsx5(
201
+ "div",
202
+ {
203
+ className: "preview",
204
+ dangerouslySetInnerHTML: { __html: markdownToHtml(value) },
205
+ style: { listStyle: "decimal" }
206
+ }
207
+ );
208
+ }
209
+
210
+ // src/constants/toolbar.ts
211
+ import {
212
+ BoldIcon,
213
+ ItalicIcon,
214
+ CodeIcon,
215
+ LinkIcon,
216
+ Heading1Icon,
217
+ Heading2Icon,
218
+ Heading3Icon,
219
+ ListOrderedIcon,
220
+ ListIcon,
221
+ QuoteIcon
222
+ } from "lucide-react";
223
+ var TOOLBAR_BUTTONS = [
224
+ { key: "bold", before: "**", after: "**", Icon: BoldIcon },
225
+ { key: "italic", before: "*", after: "*", Icon: ItalicIcon },
226
+ { key: "code", before: "`", after: "`", Icon: CodeIcon },
227
+ { key: "blockquote", before: "> ", after: "", Icon: QuoteIcon },
228
+ { key: "link", before: "[", after: "]()", Icon: LinkIcon }
229
+ ];
230
+ var headingOptions = [
231
+ { value: "h1", Icon: Heading1Icon, before: "# " },
232
+ { value: "h2", Icon: Heading2Icon, before: "## " },
233
+ { value: "h3", Icon: Heading3Icon, before: "### " }
234
+ ];
235
+ var listOptions = [
236
+ { value: "unordered list", Icon: ListIcon, before: "- " },
237
+ { value: "ordered list", Icon: ListOrderedIcon, before: "1. " }
238
+ ];
239
+
240
+ // src/components/Toolbar.tsx
241
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
242
+ function Toolbar({ insertMarkdown }) {
243
+ return /* @__PURE__ */ jsxs4("menu", { className: "toolbar-menu", children: [
244
+ TOOLBAR_BUTTONS.map(({ key, before, after, Icon }) => /* @__PURE__ */ jsx6(
245
+ "button",
246
+ {
247
+ className: "toolbar-button",
248
+ onClick: () => insertMarkdown(before, after),
249
+ children: /* @__PURE__ */ jsx6(Icon, { size: 18 })
250
+ },
251
+ key
252
+ )),
253
+ /* @__PURE__ */ jsx6(ToolbarDropdownHeading, { insertMarkdown }),
254
+ /* @__PURE__ */ jsx6(ToolbarDropdownList, { insertMarkdown })
255
+ ] });
256
+ }
257
+
258
+ // src/components/IconDropdown.tsx
259
+ import React from "react";
260
+
261
+ // src/utils/cx.ts
262
+ function cx(...classes) {
263
+ return classes.filter(Boolean).join(" ");
264
+ }
265
+
266
+ // src/components/IconDropdown.tsx
267
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
268
+ function IconDropdown({
269
+ className,
270
+ disabled,
271
+ options,
272
+ selected,
273
+ triggerIcon,
274
+ onChange
275
+ }) {
276
+ const [isOpen, setIsOpen] = React.useState(false);
277
+ const dropdownRef = React.useRef(null);
278
+ const toggle = () => {
279
+ if (!disabled) setIsOpen((prev) => !prev);
280
+ };
281
+ React.useEffect(() => {
282
+ const handleClickOutside = (e) => {
283
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
284
+ setIsOpen(false);
285
+ }
286
+ };
287
+ document.addEventListener("mousedown", handleClickOutside);
288
+ return () => document.removeEventListener("mousedown", handleClickOutside);
289
+ }, []);
290
+ return /* @__PURE__ */ jsxs5("div", { ref: dropdownRef, className: cx("md-dropdown", className), children: [
291
+ /* @__PURE__ */ jsx7(
292
+ "button",
293
+ {
294
+ type: "button",
295
+ onClick: toggle,
296
+ disabled,
297
+ className: cx(
298
+ "md-dropdown__trigger",
299
+ disabled && "md-dropdown__trigger--disabled"
300
+ ),
301
+ children: triggerIcon
302
+ }
303
+ ),
304
+ isOpen && /* @__PURE__ */ jsx7("ul", { className: "md-dropdown__menu", children: options.map((option) => /* @__PURE__ */ jsxs5(
305
+ "li",
306
+ {
307
+ className: cx(
308
+ "md-dropdown__item",
309
+ selected && "md-dropdown__item--selected"
310
+ ),
311
+ onClick: () => {
312
+ onChange(option.value);
313
+ setIsOpen(false);
314
+ },
315
+ children: [
316
+ option.Icon && /* @__PURE__ */ jsx7("span", { className: "md-dropdown__icon", children: /* @__PURE__ */ jsx7(option.Icon, {}) }),
317
+ option.label && /* @__PURE__ */ jsx7("span", { className: "md-dropdown__label", children: option.label })
318
+ ]
319
+ },
320
+ option.value
321
+ )) })
322
+ ] });
323
+ }
324
+
325
+ // src/components/ToolbarDropdown.tsx
326
+ import { Heading1Icon as Heading1Icon2, ListIcon as ListIcon2 } from "lucide-react";
327
+ import { jsx as jsx8 } from "react/jsx-runtime";
328
+ function ToolbarDropdownHeading({
329
+ insertMarkdown
330
+ }) {
331
+ return /* @__PURE__ */ jsx8(
332
+ IconDropdown,
333
+ {
334
+ options: headingOptions,
335
+ triggerIcon: /* @__PURE__ */ jsx8(Heading1Icon2, {}),
336
+ onChange: (value) => {
337
+ const option = headingOptions.find((o) => o.value === value);
338
+ if (option) insertMarkdown(option.before);
339
+ }
340
+ }
341
+ );
342
+ }
343
+ function ToolbarDropdownList({ insertMarkdown }) {
344
+ return /* @__PURE__ */ jsx8(
345
+ IconDropdown,
346
+ {
347
+ options: listOptions,
348
+ triggerIcon: /* @__PURE__ */ jsx8(ListIcon2, {}),
349
+ onChange: (value) => {
350
+ const option = listOptions.find((o) => o.value === value);
351
+ if (option) insertMarkdown(option.before);
352
+ }
353
+ }
354
+ );
355
+ }
356
+ export {
357
+ EditorHeader,
358
+ EditorTab,
359
+ EditorTextarea,
360
+ MarkdownEditor,
361
+ Preview,
362
+ TOOLBAR_BUTTONS,
363
+ Toolbar,
364
+ ToolbarDropdownHeading,
365
+ ToolbarDropdownList,
366
+ cx,
367
+ headingOptions,
368
+ listOptions,
369
+ markdownToHtml,
370
+ useMarkdownEditor
371
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "md-editor-lite",
3
+ "description": "A lightweight, dependency-minimal React markdown editor",
4
+ "keywords": [
5
+ "markdown",
6
+ "editor",
7
+ "react",
8
+ "lightweight",
9
+ "textarea"
10
+ ],
11
+ "version": "0.1.0",
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.mjs",
14
+ "types": "dist/index.d.ts",
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.build.json"
17
+ },
18
+ "peerDependencies": {
19
+ "react": ">=18"
20
+ },
21
+ "devDependencies": {
22
+ "@types/react": "^19.2.7",
23
+ "@types/react-dom": "^19.2.3",
24
+ "tsup": "^8.5.1",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "dependencies": {
28
+ "lucide-react": "^0.562.0"
29
+ }
30
+ }
@@ -0,0 +1,19 @@
1
+ import { useState } from "react";
2
+ import { MarkdownEditor } from "../src";
3
+
4
+ export default function App() {
5
+ const [value, setValue] = useState("");
6
+
7
+ return (
8
+ <div
9
+ style={{
10
+ padding: 40,
11
+ height: "100vh",
12
+ width: "100vw",
13
+ boxSizing: "border-box",
14
+ }}
15
+ >
16
+ <MarkdownEditor value={value} onChange={setValue} />
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>md-editor-lite playground</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App.tsx";
4
+
5
+ createRoot(document.getElementById("root")!).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>
9
+ );