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/README.md +175 -0
- package/dist/index.css +181 -0
- package/dist/index.d.mts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +410 -0
- package/dist/index.mjs +371 -0
- package/package.json +30 -0
- package/playground/App.tsx +19 -0
- package/playground/index.html +12 -0
- package/playground/main.tsx +9 -0
- package/playground/package-lock.json +970 -0
- package/playground/package.json +14 -0
- package/playground/tsconfig.app.json +31 -0
- package/playground/tsconfig.json +7 -0
- package/playground/tsconfig.node.json +24 -0
- package/playground/vite.config.ts +6 -0
- package/src/components/EditorHeader.tsx +21 -0
- package/src/components/EditorTab.tsx +27 -0
- package/src/components/EditorTextarea.tsx +39 -0
- package/src/components/IconDropdown.tsx +90 -0
- package/src/components/MarkdownEditor.tsx +42 -0
- package/src/components/Preview.tsx +16 -0
- package/src/components/Toolbar.tsx +25 -0
- package/src/components/ToolbarDropdown.tsx +35 -0
- package/src/components/index.ts +7 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/toolbar.ts +36 -0
- package/src/css/dropdown.css +101 -0
- package/src/css/editor.css +67 -0
- package/src/css/preview.css +11 -0
- package/src/css/toolbar.css +25 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useMarkdownEditor.ts +39 -0
- package/src/index.ts +4 -0
- package/src/utils/cx.ts +3 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/markdown.ts +89 -0
- package/tsconfig.build.json +18 -0
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
|
+

|
|
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>
|