@dskripchenko/markdown-editor 0.0.0-stage → 0.2.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/index.cjs +420 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.mjs +411 -0
- package/dist/style.css +99 -0
- package/package.json +87 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 127c7b2: `image-base` and `link-base`: a document's relative pictures show from its folder in the visual editor and the preview and are saved back as written (`img/a.png`, `../a.png`); relative links of the preview resolve against `link-base`. `fromMarkdown`, `toMarkdown` and `normalize` take `{ imageBase }`; `relativeTo` is exported.
|
|
8
|
+
|
|
9
|
+
## 0.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 37932ce: The first release: `MarkdownEditor` — Markdown as the value, edited in the `@dskripchenko/wysiwyg` editor, with Markdown and side-by-side modes; a lossless round trip that keeps what the editor cannot hold (headings below h3, task lists, callouts, HTML blocks, footnotes, reference links) as written; Markdown pasted as plain text arrives formatted; `fromMarkdown`, `toMarkdown`, `normalize`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denis Skripchenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dskripchenko/markdown-editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@dskripchenko/markdown-editor)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A Markdown editor for Vue 3 that edits like a word processor. The value is
|
|
7
|
+
**Markdown**; the editing is the [`@dskripchenko/wysiwyg`](https://github.com/dskripchenko/wysiwyg)
|
|
8
|
+
engine — its toolbar, slash menu, shortcuts and live blocks — with a
|
|
9
|
+
**Markdown** mode and a **side-by-side** mode with a live preview.
|
|
10
|
+
|
|
11
|
+
[**English**](docs/en/getting-started.md) · [Русский](docs/ru/getting-started.md) · [Deutsch](docs/de/getting-started.md) · [中文](docs/zh/getting-started.md)
|
|
12
|
+
|
|
13
|
+
```vue
|
|
14
|
+
<script setup>
|
|
15
|
+
import { ref } from 'vue'
|
|
16
|
+
import { MarkdownEditor } from '@dskripchenko/markdown-editor'
|
|
17
|
+
import '@dskripchenko/ui/styles/all.css'
|
|
18
|
+
import '@dskripchenko/wysiwyg/style.css'
|
|
19
|
+
import '@dskripchenko/markdown-editor/style.css'
|
|
20
|
+
|
|
21
|
+
const text = ref('# Release notes\n\nVersion **3.1**.')
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<MarkdownEditor v-model="text" />
|
|
26
|
+
</template>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **A lossless round trip.** Markdown goes into the editor and comes back as
|
|
30
|
+
Markdown: the first save puts it in canonical form (`-` for lists, `**`
|
|
31
|
+
for bold), every later one changes nothing. Plain text keeps its dots,
|
|
32
|
+
dashes and brackets.
|
|
33
|
+
- **Nothing is lost.** What the visual editor cannot hold — headings below
|
|
34
|
+
h3, task lists, callouts, HTML blocks, footnotes, reference-style links —
|
|
35
|
+
stays as written: shown as a block of Markdown and saved byte for byte.
|
|
36
|
+
- **Pasting Markdown** as plain text gives formatted text; pasting from a
|
|
37
|
+
page or a document is cleaned by the editor's sanitizer.
|
|
38
|
+
- Live blocks (```` ```mermaid ```` and others) through `blockRenderers` —
|
|
39
|
+
the same renderers in the editor and in the preview.
|
|
40
|
+
- No runtime dependencies: Vue, [`@dskripchenko/ui`](https://github.com/dskripchenko/ui)
|
|
41
|
+
and `@dskripchenko/wysiwyg` are peer dependencies. Russian and English.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm i @dskripchenko/markdown-editor @dskripchenko/wysiwyg @dskripchenko/ui vue
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
→ [Getting started](docs/en/getting-started.md): props, events, modes,
|
|
50
|
+
the round trip, the conversion functions.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let vue = require("vue");
|
|
3
|
+
let _dskripchenko_ui = require("@dskripchenko/ui");
|
|
4
|
+
let _dskripchenko_wysiwyg = require("@dskripchenko/wysiwyg");
|
|
5
|
+
// src/convert.ts
|
|
6
|
+
/**
|
|
7
|
+
* Markdown ↔ the editor's HTML.
|
|
8
|
+
*
|
|
9
|
+
* `fromMarkdown` renders Markdown with the kit's renderer into the HTML the
|
|
10
|
+
* WYSIWYG editor edits; `toMarkdown` writes the editor's HTML back with the
|
|
11
|
+
* editor's own converter. What the editor cannot hold — headings below h3,
|
|
12
|
+
* task lists, callouts, HTML blocks, footnotes, reference-style links — is
|
|
13
|
+
* kept as a source block: a code block of the language {@link SOURCE_LANG}
|
|
14
|
+
* that shows the Markdown as written and comes back out byte for byte.
|
|
15
|
+
*
|
|
16
|
+
* The first round normalises the document (list markers, emphasis markers,
|
|
17
|
+
* spacing); every later round changes nothing.
|
|
18
|
+
*/
|
|
19
|
+
/** The code-block language of Markdown kept as written. */
|
|
20
|
+
var SOURCE_LANG = "x-markdown";
|
|
21
|
+
var FENCE = /^\s{0,3}(`{3,}|~{3,})/;
|
|
22
|
+
var LIST_ITEM = /^(\s*)([-*+]|\d+[.)])\s+/;
|
|
23
|
+
var TASK_ITEM = /^\s*([-*+]|\d+[.)])\s+\[[ xX]\]\s/;
|
|
24
|
+
var HTML_BLOCK = /^\s{0,3}<(?:[A-Za-z][\w-]*[\s/>]|[A-Za-z][\w-]*$|\/[A-Za-z]|!--)/;
|
|
25
|
+
var FOOTNOTE = /^\s{0,3}\[\^[^\]]+\]:/;
|
|
26
|
+
var LINK_DEFINITION = /^\s{0,3}\[[^\]^][^\]]*\]:\s*\S/;
|
|
27
|
+
var REFERENCE = /\]\[[^\]]*\]|\[[^\]]+\]\[\]/;
|
|
28
|
+
/**
|
|
29
|
+
* The document in top-level blocks, each marked as edited in the editor or
|
|
30
|
+
* kept as written. Blank lines between blocks belong to no block.
|
|
31
|
+
*/
|
|
32
|
+
function splitBlocks(markdown) {
|
|
33
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
34
|
+
const blocks = [];
|
|
35
|
+
let i = 0;
|
|
36
|
+
const take = (from, to, source) => {
|
|
37
|
+
blocks.push({
|
|
38
|
+
lines: lines.slice(from, to),
|
|
39
|
+
source
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
while (i < lines.length) {
|
|
43
|
+
const line = lines[i] ?? "";
|
|
44
|
+
if (line.trim() === "") {
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const start = i;
|
|
49
|
+
const fence = FENCE.exec(line);
|
|
50
|
+
if (fence) {
|
|
51
|
+
const marker = fence[1] ?? "```";
|
|
52
|
+
const indent = line.length - line.trimStart().length;
|
|
53
|
+
i++;
|
|
54
|
+
while (i < lines.length) {
|
|
55
|
+
const l = lines[i] ?? "";
|
|
56
|
+
const t = l.trim();
|
|
57
|
+
i++;
|
|
58
|
+
if (l.length - l.trimStart().length <= indent + 3 && t.length >= marker.length && t[0] === marker[0] && /^(`+|~+)$/.test(t)) break;
|
|
59
|
+
}
|
|
60
|
+
take(start, i, false);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (LIST_ITEM.test(line)) {
|
|
64
|
+
i++;
|
|
65
|
+
while (i < lines.length) {
|
|
66
|
+
const l = lines[i] ?? "";
|
|
67
|
+
if (l.trim() === "") {
|
|
68
|
+
const next = lines[i + 1] ?? "";
|
|
69
|
+
if (next.trim() !== "" && (LIST_ITEM.test(next) || /^\s{2,}\S/.test(next))) {
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (LIST_ITEM.test(l) || /^\s{2,}\S/.test(l)) {
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const block = lines.slice(start, i);
|
|
82
|
+
take(start, i, block.some((l) => TASK_ITEM.test(l)) || block.some((l) => REFERENCE.test(l)));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (/^\s{0,3}>/.test(line)) {
|
|
86
|
+
i++;
|
|
87
|
+
while (i < lines.length && /^\s{0,3}>/.test(lines[i] ?? "")) i++;
|
|
88
|
+
const block = lines.slice(start, i);
|
|
89
|
+
take(start, i, /^\s{0,3}>\s*\[![A-Za-z]+\]/.test(line) || block.some((l) => REFERENCE.test(l)));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (/^\s{0,3}#{4,6}(\s|$)/.test(line)) {
|
|
93
|
+
take(start, ++i, true);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (FOOTNOTE.test(line) || LINK_DEFINITION.test(line)) {
|
|
97
|
+
i++;
|
|
98
|
+
while (i < lines.length && (/^\s{2,}\S/.test(lines[i] ?? "") || FOOTNOTE.test(lines[i] ?? "") || LINK_DEFINITION.test(lines[i] ?? ""))) i++;
|
|
99
|
+
take(start, i, true);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (HTML_BLOCK.test(line)) {
|
|
103
|
+
i++;
|
|
104
|
+
while (i < lines.length && (lines[i] ?? "").trim() !== "") i++;
|
|
105
|
+
take(start, i, true);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
i++;
|
|
109
|
+
if (!/^\s{0,3}#{1,3}\s/.test(line)) while (i < lines.length) {
|
|
110
|
+
const l = lines[i] ?? "";
|
|
111
|
+
if (l.trim() === "" || FENCE.test(l) || /^\s{0,3}(#{1,6}\s|>)/.test(l) || LIST_ITEM.test(l) && !/^\s*\d+[.)]/.test(l) || HTML_BLOCK.test(l)) break;
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
const block = lines.slice(start, i);
|
|
115
|
+
take(start, i, block.some((l) => REFERENCE.test(l)) || block.some((l) => /\[\^[^\]]+\]/.test(l)));
|
|
116
|
+
}
|
|
117
|
+
return blocks.map((b) => ({
|
|
118
|
+
kind: b.source ? "source" : "markdown",
|
|
119
|
+
text: b.lines.join("\n")
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
function escapeHtml(text) {
|
|
123
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A URL back to a path relative to `base`, when both share the origin:
|
|
127
|
+
* `<base>img/a.png` → `img/a.png`, `<base's parent>up.png` → `../up.png`.
|
|
128
|
+
* Anything else stays as it is.
|
|
129
|
+
*/
|
|
130
|
+
function relativeTo(url, base) {
|
|
131
|
+
let u;
|
|
132
|
+
let b;
|
|
133
|
+
try {
|
|
134
|
+
u = new URL(url);
|
|
135
|
+
b = new URL(base);
|
|
136
|
+
} catch {
|
|
137
|
+
return url;
|
|
138
|
+
}
|
|
139
|
+
if (u.protocol !== b.protocol || u.host !== b.host) return url;
|
|
140
|
+
const from = b.pathname.split("/").slice(0, -1);
|
|
141
|
+
const to = u.pathname.split("/");
|
|
142
|
+
let i = 0;
|
|
143
|
+
while (i < from.length && i < to.length - 1 && from[i] === to[i]) i++;
|
|
144
|
+
if (i === 0 && from.length > 1) return url;
|
|
145
|
+
const rel = [...Array(from.length - i).fill(".."), ...to.slice(i)].join("/");
|
|
146
|
+
return decodeURI(rel) + u.search + u.hash;
|
|
147
|
+
}
|
|
148
|
+
/** Markdown → the HTML the editor edits; source blocks become code blocks of {@link SOURCE_LANG}. */
|
|
149
|
+
function fromMarkdown(markdown, options = {}) {
|
|
150
|
+
const out = [];
|
|
151
|
+
let pending = [];
|
|
152
|
+
const flush = () => {
|
|
153
|
+
if (pending.length) out.push((0, _dskripchenko_ui.renderMarkdown)(pending.join("\n\n"), options.imageBase ? { imageBase: options.imageBase } : {}));
|
|
154
|
+
pending = [];
|
|
155
|
+
};
|
|
156
|
+
for (const block of splitBlocks(markdown)) {
|
|
157
|
+
if (block.kind === "markdown") {
|
|
158
|
+
pending.push(block.text);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
flush();
|
|
162
|
+
out.push(`<pre><code class="language-${SOURCE_LANG}">${escapeHtml(block.text)}</code></pre>`);
|
|
163
|
+
}
|
|
164
|
+
flush();
|
|
165
|
+
return out.join("\n");
|
|
166
|
+
}
|
|
167
|
+
var SOURCE_FENCE = new RegExp(`^(\`{3,})${SOURCE_LANG}\\n([\\s\\S]*?)\\n\\1$`, "gm");
|
|
168
|
+
/** The editor's HTML → Markdown; source blocks come back as written, pictures by their relative paths. */
|
|
169
|
+
function toMarkdown(html, options = {}) {
|
|
170
|
+
const base = options.imageBase;
|
|
171
|
+
const relative = base ? html.replace(/(<img\b[^>]*?\ssrc=")([^"]*)(")/gi, (_m, head, src, tail) => {
|
|
172
|
+
return head + relativeTo(src.replace(/&/g, "&"), base).replace(/&/g, "&") + tail;
|
|
173
|
+
}) : html;
|
|
174
|
+
const md = (0, _dskripchenko_wysiwyg.htmlToMarkdown)(relative).replace(SOURCE_FENCE, (_m, _fence, text) => text);
|
|
175
|
+
return md === "" ? "" : `${md}\n`;
|
|
176
|
+
}
|
|
177
|
+
/** One round: what saving a document unchanged in the editor gives. */
|
|
178
|
+
function normalize(markdown, options = {}) {
|
|
179
|
+
return toMarkdown(fromMarkdown(markdown, options), options);
|
|
180
|
+
}
|
|
181
|
+
// src/locale.ts
|
|
182
|
+
var en = {
|
|
183
|
+
visual: "Visual",
|
|
184
|
+
source: "Markdown",
|
|
185
|
+
split: "Side by side",
|
|
186
|
+
modes: "Editing mode",
|
|
187
|
+
preview: "Preview",
|
|
188
|
+
sourceBlock: "Markdown kept as written — edit it in the Markdown mode"
|
|
189
|
+
};
|
|
190
|
+
var ru = {
|
|
191
|
+
visual: "Визуально",
|
|
192
|
+
source: "Markdown",
|
|
193
|
+
split: "Рядом",
|
|
194
|
+
modes: "Режим правки",
|
|
195
|
+
preview: "Просмотр",
|
|
196
|
+
sourceBlock: "Markdown как написан — правится в режиме Markdown"
|
|
197
|
+
};
|
|
198
|
+
var markdownEditorLocales = {
|
|
199
|
+
en,
|
|
200
|
+
ru
|
|
201
|
+
};
|
|
202
|
+
function labelsFor(locale) {
|
|
203
|
+
return locale?.toLowerCase().startsWith("ru") ? ru : en;
|
|
204
|
+
}
|
|
205
|
+
// src/MarkdownEditor.vue?vue&type=script&setup=true&lang.ts
|
|
206
|
+
var _hoisted_1 = ["aria-label"];
|
|
207
|
+
var _hoisted_2 = ["aria-pressed", "onClick"];
|
|
208
|
+
var _hoisted_3 = ["data-source-label"];
|
|
209
|
+
var _hoisted_4 = {
|
|
210
|
+
key: 2,
|
|
211
|
+
class: "dsk-md__panes"
|
|
212
|
+
};
|
|
213
|
+
var _hoisted_5 = [
|
|
214
|
+
"value",
|
|
215
|
+
"placeholder",
|
|
216
|
+
"readonly",
|
|
217
|
+
"aria-label"
|
|
218
|
+
];
|
|
219
|
+
var _hoisted_6 = ["aria-label"];
|
|
220
|
+
// src/MarkdownEditor.vue
|
|
221
|
+
var MarkdownEditor_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
222
|
+
__name: "MarkdownEditor",
|
|
223
|
+
props: /*@__PURE__*/ (0, vue.mergeModels)({
|
|
224
|
+
modelValue: { default: "" },
|
|
225
|
+
modes: { default: () => [
|
|
226
|
+
"visual",
|
|
227
|
+
"source",
|
|
228
|
+
"split"
|
|
229
|
+
] },
|
|
230
|
+
placeholder: { default: void 0 },
|
|
231
|
+
readonly: {
|
|
232
|
+
type: Boolean,
|
|
233
|
+
default: false
|
|
234
|
+
},
|
|
235
|
+
minHeight: { default: "240px" },
|
|
236
|
+
maxHeight: { default: void 0 },
|
|
237
|
+
blockRenderers: { default: void 0 },
|
|
238
|
+
toolbarItems: { default: void 0 },
|
|
239
|
+
slashCommands: { default: void 0 },
|
|
240
|
+
locale: { default: void 0 },
|
|
241
|
+
labels: { default: void 0 },
|
|
242
|
+
imageBase: { default: void 0 },
|
|
243
|
+
linkBase: { default: void 0 }
|
|
244
|
+
}, {
|
|
245
|
+
"mode": { default: "visual" },
|
|
246
|
+
"modeModifiers": {}
|
|
247
|
+
}),
|
|
248
|
+
emits: /*@__PURE__*/ (0, vue.mergeModels)(["update:modelValue", "change"], ["update:mode"]),
|
|
249
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
250
|
+
const props = __props;
|
|
251
|
+
const emit = __emit;
|
|
252
|
+
const mode = (0, vue.useModel)(__props, "mode");
|
|
253
|
+
const kitLocale = (0, _dskripchenko_ui.useLocale)();
|
|
254
|
+
const t = (0, vue.computed)(() => ({
|
|
255
|
+
...labelsFor(props.locale ?? kitLocale.value.code),
|
|
256
|
+
...props.labels
|
|
257
|
+
}));
|
|
258
|
+
const options = (0, vue.computed)(() => ({ imageBase: props.imageBase ?? null }));
|
|
259
|
+
const markdown = (0, vue.ref)(props.modelValue);
|
|
260
|
+
const html = (0, vue.ref)(fromMarkdown(props.modelValue, options.value));
|
|
261
|
+
const wysiwyg = (0, vue.ref)(null);
|
|
262
|
+
const area = (0, vue.ref)(null);
|
|
263
|
+
(0, vue.watch)(() => props.modelValue, (next) => {
|
|
264
|
+
if (next === markdown.value) return;
|
|
265
|
+
markdown.value = next;
|
|
266
|
+
html.value = fromMarkdown(next, options.value);
|
|
267
|
+
});
|
|
268
|
+
(0, vue.watch)(() => props.imageBase, () => {
|
|
269
|
+
html.value = fromMarkdown(markdown.value, options.value);
|
|
270
|
+
});
|
|
271
|
+
function commit(next) {
|
|
272
|
+
if (next === markdown.value) return;
|
|
273
|
+
markdown.value = next;
|
|
274
|
+
emit("update:modelValue", next);
|
|
275
|
+
emit("change", next);
|
|
276
|
+
}
|
|
277
|
+
function onHtml(next) {
|
|
278
|
+
html.value = next;
|
|
279
|
+
commit(toMarkdown(next, options.value));
|
|
280
|
+
}
|
|
281
|
+
function onSource(event) {
|
|
282
|
+
commit(event.target.value);
|
|
283
|
+
}
|
|
284
|
+
function setMode(next) {
|
|
285
|
+
if (next === mode.value) return;
|
|
286
|
+
if (next === "visual") html.value = fromMarkdown(markdown.value, options.value);
|
|
287
|
+
mode.value = next;
|
|
288
|
+
}
|
|
289
|
+
/** Text that reads as Markdown: pasted into the visual editor, it arrives formatted. */
|
|
290
|
+
const MARKDOWN_HINT = /(^|\n)\s{0,3}(#{1,6}\s|[-*+]\s|\d+[.)]\s|>|```|~~~|\|.*\|)|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\)|`[^`]+`/;
|
|
291
|
+
function onPaste(event) {
|
|
292
|
+
if (props.readonly || mode.value !== "visual") return;
|
|
293
|
+
const data = event.clipboardData;
|
|
294
|
+
if (!data || data.types.includes("text/html")) return;
|
|
295
|
+
const text = data.getData("text/plain");
|
|
296
|
+
if (!text || !MARKDOWN_HINT.test(text)) return;
|
|
297
|
+
event.preventDefault();
|
|
298
|
+
event.stopPropagation();
|
|
299
|
+
document.execCommand("insertHTML", false, fromMarkdown(text, options.value));
|
|
300
|
+
}
|
|
301
|
+
function onTab(event) {
|
|
302
|
+
if (event.key !== "Tab" || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
const el = event.target;
|
|
305
|
+
el.setRangeText(" ", el.selectionStart, el.selectionEnd, "end");
|
|
306
|
+
commit(el.value);
|
|
307
|
+
}
|
|
308
|
+
/** Focus the editor of the current mode. */
|
|
309
|
+
function focus() {
|
|
310
|
+
if (mode.value === "visual") wysiwyg.value?.controller?.chain().focus().run();
|
|
311
|
+
else area.value?.focus();
|
|
312
|
+
}
|
|
313
|
+
/** Insert Markdown at the caret (the source's caret in the Markdown modes). */
|
|
314
|
+
function insert(text) {
|
|
315
|
+
if (mode.value === "visual") {
|
|
316
|
+
focus();
|
|
317
|
+
document.execCommand("insertHTML", false, fromMarkdown(text, options.value));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const el = area.value;
|
|
321
|
+
if (!el) return;
|
|
322
|
+
el.focus();
|
|
323
|
+
el.setRangeText(text, el.selectionStart, el.selectionEnd, "end");
|
|
324
|
+
commit(el.value);
|
|
325
|
+
}
|
|
326
|
+
__expose({
|
|
327
|
+
focus,
|
|
328
|
+
insert,
|
|
329
|
+
get value() {
|
|
330
|
+
return markdown.value;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
return (_ctx, _cache) => {
|
|
334
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
335
|
+
class: (0, vue.normalizeClass)(["dsk-md", [`dsk-md--${mode.value}`, { "dsk-md--readonly": __props.readonly }]]),
|
|
336
|
+
style: (0, vue.normalizeStyle)({
|
|
337
|
+
"--dsk-md-min-h": __props.minHeight,
|
|
338
|
+
"--dsk-md-max-h": __props.maxHeight
|
|
339
|
+
})
|
|
340
|
+
}, [__props.modes.length > 1 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
341
|
+
key: 0,
|
|
342
|
+
class: "dsk-md__modes",
|
|
343
|
+
role: "group",
|
|
344
|
+
"aria-label": t.value.modes
|
|
345
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.modes, (m) => {
|
|
346
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
347
|
+
key: m,
|
|
348
|
+
type: "button",
|
|
349
|
+
class: (0, vue.normalizeClass)(["dsk-md__mode", { "dsk-md__mode--on": m === mode.value }]),
|
|
350
|
+
"aria-pressed": m === mode.value,
|
|
351
|
+
onClick: ($event) => setMode(m)
|
|
352
|
+
}, (0, vue.toDisplayString)(t.value[m]), 11, _hoisted_2);
|
|
353
|
+
}), 128))], 8, _hoisted_1)) : (0, vue.createCommentVNode)("", true), mode.value === "visual" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
354
|
+
key: 1,
|
|
355
|
+
class: "dsk-md__visual",
|
|
356
|
+
"data-source-label": t.value.sourceBlock,
|
|
357
|
+
onPasteCapture: onPaste
|
|
358
|
+
}, [(0, vue.createVNode)((0, vue.unref)(_dskripchenko_wysiwyg.DskWysiwyg), {
|
|
359
|
+
ref_key: "wysiwyg",
|
|
360
|
+
ref: wysiwyg,
|
|
361
|
+
"model-value": html.value,
|
|
362
|
+
placeholder: __props.placeholder,
|
|
363
|
+
readonly: __props.readonly,
|
|
364
|
+
"min-height": __props.minHeight,
|
|
365
|
+
"max-height": __props.maxHeight,
|
|
366
|
+
"block-renderers": __props.blockRenderers,
|
|
367
|
+
"toolbar-items": __props.toolbarItems,
|
|
368
|
+
"slash-commands": __props.slashCommands,
|
|
369
|
+
locale: __props.locale,
|
|
370
|
+
"onUpdate:modelValue": onHtml
|
|
371
|
+
}, null, 8, [
|
|
372
|
+
"model-value",
|
|
373
|
+
"placeholder",
|
|
374
|
+
"readonly",
|
|
375
|
+
"min-height",
|
|
376
|
+
"max-height",
|
|
377
|
+
"block-renderers",
|
|
378
|
+
"toolbar-items",
|
|
379
|
+
"slash-commands",
|
|
380
|
+
"locale"
|
|
381
|
+
])], 40, _hoisted_3)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_4, [(0, vue.createElementVNode)("textarea", {
|
|
382
|
+
ref_key: "area",
|
|
383
|
+
ref: area,
|
|
384
|
+
class: "dsk-md__source",
|
|
385
|
+
value: markdown.value,
|
|
386
|
+
placeholder: __props.placeholder,
|
|
387
|
+
readonly: __props.readonly,
|
|
388
|
+
"aria-label": t.value.source,
|
|
389
|
+
spellcheck: "false",
|
|
390
|
+
onInput: onSource,
|
|
391
|
+
onKeydown: onTab
|
|
392
|
+
}, null, 40, _hoisted_5), mode.value === "split" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
393
|
+
key: 0,
|
|
394
|
+
class: "dsk-md__preview",
|
|
395
|
+
"aria-label": t.value.preview
|
|
396
|
+
}, [(0, vue.createVNode)((0, vue.unref)(_dskripchenko_ui.UidMarkdown), {
|
|
397
|
+
source: markdown.value,
|
|
398
|
+
"block-renderers": __props.blockRenderers,
|
|
399
|
+
"image-base": __props.imageBase,
|
|
400
|
+
"link-base": __props.linkBase
|
|
401
|
+
}, null, 8, [
|
|
402
|
+
"source",
|
|
403
|
+
"block-renderers",
|
|
404
|
+
"image-base",
|
|
405
|
+
"link-base"
|
|
406
|
+
])], 8, _hoisted_6)) : (0, vue.createCommentVNode)("", true)]))], 6);
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
exports.MarkdownEditor = MarkdownEditor_default;
|
|
411
|
+
exports.SOURCE_LANG = SOURCE_LANG;
|
|
412
|
+
exports.fromMarkdown = fromMarkdown;
|
|
413
|
+
exports.labelsFor = labelsFor;
|
|
414
|
+
exports.markdownEditorLocales = markdownEditorLocales;
|
|
415
|
+
exports.normalize = normalize;
|
|
416
|
+
exports.relativeTo = relativeTo;
|
|
417
|
+
exports.splitBlocks = splitBlocks;
|
|
418
|
+
exports.toMarkdown = toMarkdown;
|
|
419
|
+
|
|
420
|
+
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { BlockRenderer } from '@dskripchenko/wysiwyg';
|
|
2
|
+
import { ComponentOptionsMixin } from 'vue';
|
|
3
|
+
import { ComponentProvideOptions } from 'vue';
|
|
4
|
+
import { DefineComponent } from 'vue';
|
|
5
|
+
import { DskWysiwyg } from '@dskripchenko/wysiwyg';
|
|
6
|
+
import { PublicProps } from 'vue';
|
|
7
|
+
import { SlashCommand } from '@dskripchenko/wysiwyg';
|
|
8
|
+
import { ToolbarItem } from '@dskripchenko/wysiwyg';
|
|
9
|
+
|
|
10
|
+
declare const __VLS_export: DefineComponent<__VLS_PublicProps, {
|
|
11
|
+
focus: typeof focus_2;
|
|
12
|
+
insert: typeof insert;
|
|
13
|
+
readonly value: string;
|
|
14
|
+
}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
|
|
15
|
+
"update:modelValue": (markdown: string) => any;
|
|
16
|
+
change: (markdown: string) => any;
|
|
17
|
+
"update:mode": (value: MarkdownEditorMode) => any;
|
|
18
|
+
}, string, PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
19
|
+
"onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
|
|
20
|
+
onChange?: ((markdown: string) => any) | undefined;
|
|
21
|
+
"onUpdate:mode"?: ((value: MarkdownEditorMode) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
imageBase: string;
|
|
24
|
+
modelValue: string;
|
|
25
|
+
placeholder: string;
|
|
26
|
+
toolbarItems: ToolbarItem[];
|
|
27
|
+
readonly: boolean;
|
|
28
|
+
minHeight: string;
|
|
29
|
+
maxHeight: string;
|
|
30
|
+
blockRenderers: Readonly<Record<string, BlockRenderer>>;
|
|
31
|
+
slashCommands: SlashCommand[];
|
|
32
|
+
locale: MarkdownEditorLocale;
|
|
33
|
+
labels: Partial<MarkdownEditorLabels>;
|
|
34
|
+
modes: MarkdownEditorMode[];
|
|
35
|
+
linkBase: string;
|
|
36
|
+
}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
|
|
37
|
+
|
|
38
|
+
declare type __VLS_ModelProps = {
|
|
39
|
+
'mode'?: MarkdownEditorMode;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
declare type __VLS_Props = {
|
|
43
|
+
modelValue?: string;
|
|
44
|
+
modes?: MarkdownEditorMode[];
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
readonly?: boolean;
|
|
47
|
+
minHeight?: string;
|
|
48
|
+
maxHeight?: string;
|
|
49
|
+
blockRenderers?: BlockRenderers;
|
|
50
|
+
toolbarItems?: ToolbarItems;
|
|
51
|
+
slashCommands?: SlashCommands;
|
|
52
|
+
locale?: MarkdownEditorLocale;
|
|
53
|
+
labels?: Partial<MarkdownEditorLabels>;
|
|
54
|
+
/** The folder relative pictures resolve against (a URL ending in `/`); saved back relative. */
|
|
55
|
+
imageBase?: string;
|
|
56
|
+
/** The base of relative links in the preview. */
|
|
57
|
+
linkBase?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
declare type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
61
|
+
|
|
62
|
+
declare type BlockRenderers = InstanceType<typeof DskWysiwyg>['$props']['blockRenderers'];
|
|
63
|
+
|
|
64
|
+
/** Where the document's relative pictures resolve while it is edited. */
|
|
65
|
+
export declare interface ConvertOptions {
|
|
66
|
+
/** A folder URL (ending in `/`): `img/a.png` shows as `<imageBase>img/a.png` and is saved back as `img/a.png`. */
|
|
67
|
+
imageBase?: string | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Focus the editor of the current mode. */
|
|
71
|
+
declare function focus_2(): void;
|
|
72
|
+
|
|
73
|
+
/** Markdown → the HTML the editor edits; source blocks become code blocks of {@link SOURCE_LANG}. */
|
|
74
|
+
export declare function fromMarkdown(markdown: string, options?: ConvertOptions): string;
|
|
75
|
+
|
|
76
|
+
/** Insert Markdown at the caret (the source's caret in the Markdown modes). */
|
|
77
|
+
declare function insert(text: string): void;
|
|
78
|
+
|
|
79
|
+
export declare function labelsFor(locale: string | undefined): MarkdownEditorLabels;
|
|
80
|
+
|
|
81
|
+
export declare interface MarkdownBlock {
|
|
82
|
+
/** `source` — kept as written; `markdown` — edited in the editor. */
|
|
83
|
+
kind: 'markdown' | 'source';
|
|
84
|
+
text: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export declare const MarkdownEditor: typeof __VLS_export;
|
|
88
|
+
|
|
89
|
+
export declare interface MarkdownEditorLabels {
|
|
90
|
+
visual: string;
|
|
91
|
+
source: string;
|
|
92
|
+
split: string;
|
|
93
|
+
modes: string;
|
|
94
|
+
preview: string;
|
|
95
|
+
sourceBlock: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export declare type MarkdownEditorLocale = 'en' | 'ru';
|
|
99
|
+
|
|
100
|
+
export declare const markdownEditorLocales: Record<MarkdownEditorLocale, MarkdownEditorLabels>;
|
|
101
|
+
|
|
102
|
+
export declare type MarkdownEditorMode = 'visual' | 'source' | 'split';
|
|
103
|
+
|
|
104
|
+
/** One round: what saving a document unchanged in the editor gives. */
|
|
105
|
+
export declare function normalize(markdown: string, options?: ConvertOptions): string;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A URL back to a path relative to `base`, when both share the origin:
|
|
109
|
+
* `<base>img/a.png` → `img/a.png`, `<base's parent>up.png` → `../up.png`.
|
|
110
|
+
* Anything else stays as it is.
|
|
111
|
+
*/
|
|
112
|
+
export declare function relativeTo(url: string, base: string): string;
|
|
113
|
+
|
|
114
|
+
declare type SlashCommands = InstanceType<typeof DskWysiwyg>['$props']['slashCommands'];
|
|
115
|
+
|
|
116
|
+
/** The code-block language of Markdown kept as written. */
|
|
117
|
+
export declare const SOURCE_LANG = "x-markdown";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The document in top-level blocks, each marked as edited in the editor or
|
|
121
|
+
* kept as written. Blank lines between blocks belong to no block.
|
|
122
|
+
*/
|
|
123
|
+
export declare function splitBlocks(markdown: string): MarkdownBlock[];
|
|
124
|
+
|
|
125
|
+
/** The editor's HTML → Markdown; source blocks come back as written, pictures by their relative paths. */
|
|
126
|
+
export declare function toMarkdown(html: string, options?: ConvertOptions): string;
|
|
127
|
+
|
|
128
|
+
declare type ToolbarItems = InstanceType<typeof DskWysiwyg>['$props']['toolbarItems'];
|
|
129
|
+
|
|
130
|
+
export { }
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, mergeModels, normalizeClass, normalizeStyle, openBlock, ref, renderList, toDisplayString, unref, useModel, watch } from "vue";
|
|
2
|
+
import { UidMarkdown, renderMarkdown, useLocale } from "@dskripchenko/ui";
|
|
3
|
+
import { DskWysiwyg, htmlToMarkdown } from "@dskripchenko/wysiwyg";
|
|
4
|
+
// src/convert.ts
|
|
5
|
+
/**
|
|
6
|
+
* Markdown ↔ the editor's HTML.
|
|
7
|
+
*
|
|
8
|
+
* `fromMarkdown` renders Markdown with the kit's renderer into the HTML the
|
|
9
|
+
* WYSIWYG editor edits; `toMarkdown` writes the editor's HTML back with the
|
|
10
|
+
* editor's own converter. What the editor cannot hold — headings below h3,
|
|
11
|
+
* task lists, callouts, HTML blocks, footnotes, reference-style links — is
|
|
12
|
+
* kept as a source block: a code block of the language {@link SOURCE_LANG}
|
|
13
|
+
* that shows the Markdown as written and comes back out byte for byte.
|
|
14
|
+
*
|
|
15
|
+
* The first round normalises the document (list markers, emphasis markers,
|
|
16
|
+
* spacing); every later round changes nothing.
|
|
17
|
+
*/
|
|
18
|
+
/** The code-block language of Markdown kept as written. */
|
|
19
|
+
var SOURCE_LANG = "x-markdown";
|
|
20
|
+
var FENCE = /^\s{0,3}(`{3,}|~{3,})/;
|
|
21
|
+
var LIST_ITEM = /^(\s*)([-*+]|\d+[.)])\s+/;
|
|
22
|
+
var TASK_ITEM = /^\s*([-*+]|\d+[.)])\s+\[[ xX]\]\s/;
|
|
23
|
+
var HTML_BLOCK = /^\s{0,3}<(?:[A-Za-z][\w-]*[\s/>]|[A-Za-z][\w-]*$|\/[A-Za-z]|!--)/;
|
|
24
|
+
var FOOTNOTE = /^\s{0,3}\[\^[^\]]+\]:/;
|
|
25
|
+
var LINK_DEFINITION = /^\s{0,3}\[[^\]^][^\]]*\]:\s*\S/;
|
|
26
|
+
var REFERENCE = /\]\[[^\]]*\]|\[[^\]]+\]\[\]/;
|
|
27
|
+
/**
|
|
28
|
+
* The document in top-level blocks, each marked as edited in the editor or
|
|
29
|
+
* kept as written. Blank lines between blocks belong to no block.
|
|
30
|
+
*/
|
|
31
|
+
function splitBlocks(markdown) {
|
|
32
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
33
|
+
const blocks = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
const take = (from, to, source) => {
|
|
36
|
+
blocks.push({
|
|
37
|
+
lines: lines.slice(from, to),
|
|
38
|
+
source
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
while (i < lines.length) {
|
|
42
|
+
const line = lines[i] ?? "";
|
|
43
|
+
if (line.trim() === "") {
|
|
44
|
+
i++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const start = i;
|
|
48
|
+
const fence = FENCE.exec(line);
|
|
49
|
+
if (fence) {
|
|
50
|
+
const marker = fence[1] ?? "```";
|
|
51
|
+
const indent = line.length - line.trimStart().length;
|
|
52
|
+
i++;
|
|
53
|
+
while (i < lines.length) {
|
|
54
|
+
const l = lines[i] ?? "";
|
|
55
|
+
const t = l.trim();
|
|
56
|
+
i++;
|
|
57
|
+
if (l.length - l.trimStart().length <= indent + 3 && t.length >= marker.length && t[0] === marker[0] && /^(`+|~+)$/.test(t)) break;
|
|
58
|
+
}
|
|
59
|
+
take(start, i, false);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (LIST_ITEM.test(line)) {
|
|
63
|
+
i++;
|
|
64
|
+
while (i < lines.length) {
|
|
65
|
+
const l = lines[i] ?? "";
|
|
66
|
+
if (l.trim() === "") {
|
|
67
|
+
const next = lines[i + 1] ?? "";
|
|
68
|
+
if (next.trim() !== "" && (LIST_ITEM.test(next) || /^\s{2,}\S/.test(next))) {
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (LIST_ITEM.test(l) || /^\s{2,}\S/.test(l)) {
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const block = lines.slice(start, i);
|
|
81
|
+
take(start, i, block.some((l) => TASK_ITEM.test(l)) || block.some((l) => REFERENCE.test(l)));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (/^\s{0,3}>/.test(line)) {
|
|
85
|
+
i++;
|
|
86
|
+
while (i < lines.length && /^\s{0,3}>/.test(lines[i] ?? "")) i++;
|
|
87
|
+
const block = lines.slice(start, i);
|
|
88
|
+
take(start, i, /^\s{0,3}>\s*\[![A-Za-z]+\]/.test(line) || block.some((l) => REFERENCE.test(l)));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (/^\s{0,3}#{4,6}(\s|$)/.test(line)) {
|
|
92
|
+
take(start, ++i, true);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (FOOTNOTE.test(line) || LINK_DEFINITION.test(line)) {
|
|
96
|
+
i++;
|
|
97
|
+
while (i < lines.length && (/^\s{2,}\S/.test(lines[i] ?? "") || FOOTNOTE.test(lines[i] ?? "") || LINK_DEFINITION.test(lines[i] ?? ""))) i++;
|
|
98
|
+
take(start, i, true);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (HTML_BLOCK.test(line)) {
|
|
102
|
+
i++;
|
|
103
|
+
while (i < lines.length && (lines[i] ?? "").trim() !== "") i++;
|
|
104
|
+
take(start, i, true);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
i++;
|
|
108
|
+
if (!/^\s{0,3}#{1,3}\s/.test(line)) while (i < lines.length) {
|
|
109
|
+
const l = lines[i] ?? "";
|
|
110
|
+
if (l.trim() === "" || FENCE.test(l) || /^\s{0,3}(#{1,6}\s|>)/.test(l) || LIST_ITEM.test(l) && !/^\s*\d+[.)]/.test(l) || HTML_BLOCK.test(l)) break;
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
const block = lines.slice(start, i);
|
|
114
|
+
take(start, i, block.some((l) => REFERENCE.test(l)) || block.some((l) => /\[\^[^\]]+\]/.test(l)));
|
|
115
|
+
}
|
|
116
|
+
return blocks.map((b) => ({
|
|
117
|
+
kind: b.source ? "source" : "markdown",
|
|
118
|
+
text: b.lines.join("\n")
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
function escapeHtml(text) {
|
|
122
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A URL back to a path relative to `base`, when both share the origin:
|
|
126
|
+
* `<base>img/a.png` → `img/a.png`, `<base's parent>up.png` → `../up.png`.
|
|
127
|
+
* Anything else stays as it is.
|
|
128
|
+
*/
|
|
129
|
+
function relativeTo(url, base) {
|
|
130
|
+
let u;
|
|
131
|
+
let b;
|
|
132
|
+
try {
|
|
133
|
+
u = new URL(url);
|
|
134
|
+
b = new URL(base);
|
|
135
|
+
} catch {
|
|
136
|
+
return url;
|
|
137
|
+
}
|
|
138
|
+
if (u.protocol !== b.protocol || u.host !== b.host) return url;
|
|
139
|
+
const from = b.pathname.split("/").slice(0, -1);
|
|
140
|
+
const to = u.pathname.split("/");
|
|
141
|
+
let i = 0;
|
|
142
|
+
while (i < from.length && i < to.length - 1 && from[i] === to[i]) i++;
|
|
143
|
+
if (i === 0 && from.length > 1) return url;
|
|
144
|
+
const rel = [...Array(from.length - i).fill(".."), ...to.slice(i)].join("/");
|
|
145
|
+
return decodeURI(rel) + u.search + u.hash;
|
|
146
|
+
}
|
|
147
|
+
/** Markdown → the HTML the editor edits; source blocks become code blocks of {@link SOURCE_LANG}. */
|
|
148
|
+
function fromMarkdown(markdown, options = {}) {
|
|
149
|
+
const out = [];
|
|
150
|
+
let pending = [];
|
|
151
|
+
const flush = () => {
|
|
152
|
+
if (pending.length) out.push(renderMarkdown(pending.join("\n\n"), options.imageBase ? { imageBase: options.imageBase } : {}));
|
|
153
|
+
pending = [];
|
|
154
|
+
};
|
|
155
|
+
for (const block of splitBlocks(markdown)) {
|
|
156
|
+
if (block.kind === "markdown") {
|
|
157
|
+
pending.push(block.text);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
flush();
|
|
161
|
+
out.push(`<pre><code class="language-${SOURCE_LANG}">${escapeHtml(block.text)}</code></pre>`);
|
|
162
|
+
}
|
|
163
|
+
flush();
|
|
164
|
+
return out.join("\n");
|
|
165
|
+
}
|
|
166
|
+
var SOURCE_FENCE = new RegExp(`^(\`{3,})${SOURCE_LANG}\\n([\\s\\S]*?)\\n\\1$`, "gm");
|
|
167
|
+
/** The editor's HTML → Markdown; source blocks come back as written, pictures by their relative paths. */
|
|
168
|
+
function toMarkdown(html, options = {}) {
|
|
169
|
+
const base = options.imageBase;
|
|
170
|
+
const relative = base ? html.replace(/(<img\b[^>]*?\ssrc=")([^"]*)(")/gi, (_m, head, src, tail) => {
|
|
171
|
+
return head + relativeTo(src.replace(/&/g, "&"), base).replace(/&/g, "&") + tail;
|
|
172
|
+
}) : html;
|
|
173
|
+
const md = htmlToMarkdown(relative).replace(SOURCE_FENCE, (_m, _fence, text) => text);
|
|
174
|
+
return md === "" ? "" : `${md}\n`;
|
|
175
|
+
}
|
|
176
|
+
/** One round: what saving a document unchanged in the editor gives. */
|
|
177
|
+
function normalize(markdown, options = {}) {
|
|
178
|
+
return toMarkdown(fromMarkdown(markdown, options), options);
|
|
179
|
+
}
|
|
180
|
+
// src/locale.ts
|
|
181
|
+
var en = {
|
|
182
|
+
visual: "Visual",
|
|
183
|
+
source: "Markdown",
|
|
184
|
+
split: "Side by side",
|
|
185
|
+
modes: "Editing mode",
|
|
186
|
+
preview: "Preview",
|
|
187
|
+
sourceBlock: "Markdown kept as written — edit it in the Markdown mode"
|
|
188
|
+
};
|
|
189
|
+
var ru = {
|
|
190
|
+
visual: "Визуально",
|
|
191
|
+
source: "Markdown",
|
|
192
|
+
split: "Рядом",
|
|
193
|
+
modes: "Режим правки",
|
|
194
|
+
preview: "Просмотр",
|
|
195
|
+
sourceBlock: "Markdown как написан — правится в режиме Markdown"
|
|
196
|
+
};
|
|
197
|
+
var markdownEditorLocales = {
|
|
198
|
+
en,
|
|
199
|
+
ru
|
|
200
|
+
};
|
|
201
|
+
function labelsFor(locale) {
|
|
202
|
+
return locale?.toLowerCase().startsWith("ru") ? ru : en;
|
|
203
|
+
}
|
|
204
|
+
// src/MarkdownEditor.vue?vue&type=script&setup=true&lang.ts
|
|
205
|
+
var _hoisted_1 = ["aria-label"];
|
|
206
|
+
var _hoisted_2 = ["aria-pressed", "onClick"];
|
|
207
|
+
var _hoisted_3 = ["data-source-label"];
|
|
208
|
+
var _hoisted_4 = {
|
|
209
|
+
key: 2,
|
|
210
|
+
class: "dsk-md__panes"
|
|
211
|
+
};
|
|
212
|
+
var _hoisted_5 = [
|
|
213
|
+
"value",
|
|
214
|
+
"placeholder",
|
|
215
|
+
"readonly",
|
|
216
|
+
"aria-label"
|
|
217
|
+
];
|
|
218
|
+
var _hoisted_6 = ["aria-label"];
|
|
219
|
+
// src/MarkdownEditor.vue
|
|
220
|
+
var MarkdownEditor_default = /* @__PURE__ */ defineComponent({
|
|
221
|
+
__name: "MarkdownEditor",
|
|
222
|
+
props: /*@__PURE__*/ mergeModels({
|
|
223
|
+
modelValue: { default: "" },
|
|
224
|
+
modes: { default: () => [
|
|
225
|
+
"visual",
|
|
226
|
+
"source",
|
|
227
|
+
"split"
|
|
228
|
+
] },
|
|
229
|
+
placeholder: { default: void 0 },
|
|
230
|
+
readonly: {
|
|
231
|
+
type: Boolean,
|
|
232
|
+
default: false
|
|
233
|
+
},
|
|
234
|
+
minHeight: { default: "240px" },
|
|
235
|
+
maxHeight: { default: void 0 },
|
|
236
|
+
blockRenderers: { default: void 0 },
|
|
237
|
+
toolbarItems: { default: void 0 },
|
|
238
|
+
slashCommands: { default: void 0 },
|
|
239
|
+
locale: { default: void 0 },
|
|
240
|
+
labels: { default: void 0 },
|
|
241
|
+
imageBase: { default: void 0 },
|
|
242
|
+
linkBase: { default: void 0 }
|
|
243
|
+
}, {
|
|
244
|
+
"mode": { default: "visual" },
|
|
245
|
+
"modeModifiers": {}
|
|
246
|
+
}),
|
|
247
|
+
emits: /*@__PURE__*/ mergeModels(["update:modelValue", "change"], ["update:mode"]),
|
|
248
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
249
|
+
const props = __props;
|
|
250
|
+
const emit = __emit;
|
|
251
|
+
const mode = useModel(__props, "mode");
|
|
252
|
+
const kitLocale = useLocale();
|
|
253
|
+
const t = computed(() => ({
|
|
254
|
+
...labelsFor(props.locale ?? kitLocale.value.code),
|
|
255
|
+
...props.labels
|
|
256
|
+
}));
|
|
257
|
+
const options = computed(() => ({ imageBase: props.imageBase ?? null }));
|
|
258
|
+
const markdown = ref(props.modelValue);
|
|
259
|
+
const html = ref(fromMarkdown(props.modelValue, options.value));
|
|
260
|
+
const wysiwyg = ref(null);
|
|
261
|
+
const area = ref(null);
|
|
262
|
+
watch(() => props.modelValue, (next) => {
|
|
263
|
+
if (next === markdown.value) return;
|
|
264
|
+
markdown.value = next;
|
|
265
|
+
html.value = fromMarkdown(next, options.value);
|
|
266
|
+
});
|
|
267
|
+
watch(() => props.imageBase, () => {
|
|
268
|
+
html.value = fromMarkdown(markdown.value, options.value);
|
|
269
|
+
});
|
|
270
|
+
function commit(next) {
|
|
271
|
+
if (next === markdown.value) return;
|
|
272
|
+
markdown.value = next;
|
|
273
|
+
emit("update:modelValue", next);
|
|
274
|
+
emit("change", next);
|
|
275
|
+
}
|
|
276
|
+
function onHtml(next) {
|
|
277
|
+
html.value = next;
|
|
278
|
+
commit(toMarkdown(next, options.value));
|
|
279
|
+
}
|
|
280
|
+
function onSource(event) {
|
|
281
|
+
commit(event.target.value);
|
|
282
|
+
}
|
|
283
|
+
function setMode(next) {
|
|
284
|
+
if (next === mode.value) return;
|
|
285
|
+
if (next === "visual") html.value = fromMarkdown(markdown.value, options.value);
|
|
286
|
+
mode.value = next;
|
|
287
|
+
}
|
|
288
|
+
/** Text that reads as Markdown: pasted into the visual editor, it arrives formatted. */
|
|
289
|
+
const MARKDOWN_HINT = /(^|\n)\s{0,3}(#{1,6}\s|[-*+]\s|\d+[.)]\s|>|```|~~~|\|.*\|)|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\)|`[^`]+`/;
|
|
290
|
+
function onPaste(event) {
|
|
291
|
+
if (props.readonly || mode.value !== "visual") return;
|
|
292
|
+
const data = event.clipboardData;
|
|
293
|
+
if (!data || data.types.includes("text/html")) return;
|
|
294
|
+
const text = data.getData("text/plain");
|
|
295
|
+
if (!text || !MARKDOWN_HINT.test(text)) return;
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
event.stopPropagation();
|
|
298
|
+
document.execCommand("insertHTML", false, fromMarkdown(text, options.value));
|
|
299
|
+
}
|
|
300
|
+
function onTab(event) {
|
|
301
|
+
if (event.key !== "Tab" || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
const el = event.target;
|
|
304
|
+
el.setRangeText(" ", el.selectionStart, el.selectionEnd, "end");
|
|
305
|
+
commit(el.value);
|
|
306
|
+
}
|
|
307
|
+
/** Focus the editor of the current mode. */
|
|
308
|
+
function focus() {
|
|
309
|
+
if (mode.value === "visual") wysiwyg.value?.controller?.chain().focus().run();
|
|
310
|
+
else area.value?.focus();
|
|
311
|
+
}
|
|
312
|
+
/** Insert Markdown at the caret (the source's caret in the Markdown modes). */
|
|
313
|
+
function insert(text) {
|
|
314
|
+
if (mode.value === "visual") {
|
|
315
|
+
focus();
|
|
316
|
+
document.execCommand("insertHTML", false, fromMarkdown(text, options.value));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const el = area.value;
|
|
320
|
+
if (!el) return;
|
|
321
|
+
el.focus();
|
|
322
|
+
el.setRangeText(text, el.selectionStart, el.selectionEnd, "end");
|
|
323
|
+
commit(el.value);
|
|
324
|
+
}
|
|
325
|
+
__expose({
|
|
326
|
+
focus,
|
|
327
|
+
insert,
|
|
328
|
+
get value() {
|
|
329
|
+
return markdown.value;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
return (_ctx, _cache) => {
|
|
333
|
+
return openBlock(), createElementBlock("div", {
|
|
334
|
+
class: normalizeClass(["dsk-md", [`dsk-md--${mode.value}`, { "dsk-md--readonly": __props.readonly }]]),
|
|
335
|
+
style: normalizeStyle({
|
|
336
|
+
"--dsk-md-min-h": __props.minHeight,
|
|
337
|
+
"--dsk-md-max-h": __props.maxHeight
|
|
338
|
+
})
|
|
339
|
+
}, [__props.modes.length > 1 ? (openBlock(), createElementBlock("div", {
|
|
340
|
+
key: 0,
|
|
341
|
+
class: "dsk-md__modes",
|
|
342
|
+
role: "group",
|
|
343
|
+
"aria-label": t.value.modes
|
|
344
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.modes, (m) => {
|
|
345
|
+
return openBlock(), createElementBlock("button", {
|
|
346
|
+
key: m,
|
|
347
|
+
type: "button",
|
|
348
|
+
class: normalizeClass(["dsk-md__mode", { "dsk-md__mode--on": m === mode.value }]),
|
|
349
|
+
"aria-pressed": m === mode.value,
|
|
350
|
+
onClick: ($event) => setMode(m)
|
|
351
|
+
}, toDisplayString(t.value[m]), 11, _hoisted_2);
|
|
352
|
+
}), 128))], 8, _hoisted_1)) : createCommentVNode("", true), mode.value === "visual" ? (openBlock(), createElementBlock("div", {
|
|
353
|
+
key: 1,
|
|
354
|
+
class: "dsk-md__visual",
|
|
355
|
+
"data-source-label": t.value.sourceBlock,
|
|
356
|
+
onPasteCapture: onPaste
|
|
357
|
+
}, [createVNode(unref(DskWysiwyg), {
|
|
358
|
+
ref_key: "wysiwyg",
|
|
359
|
+
ref: wysiwyg,
|
|
360
|
+
"model-value": html.value,
|
|
361
|
+
placeholder: __props.placeholder,
|
|
362
|
+
readonly: __props.readonly,
|
|
363
|
+
"min-height": __props.minHeight,
|
|
364
|
+
"max-height": __props.maxHeight,
|
|
365
|
+
"block-renderers": __props.blockRenderers,
|
|
366
|
+
"toolbar-items": __props.toolbarItems,
|
|
367
|
+
"slash-commands": __props.slashCommands,
|
|
368
|
+
locale: __props.locale,
|
|
369
|
+
"onUpdate:modelValue": onHtml
|
|
370
|
+
}, null, 8, [
|
|
371
|
+
"model-value",
|
|
372
|
+
"placeholder",
|
|
373
|
+
"readonly",
|
|
374
|
+
"min-height",
|
|
375
|
+
"max-height",
|
|
376
|
+
"block-renderers",
|
|
377
|
+
"toolbar-items",
|
|
378
|
+
"slash-commands",
|
|
379
|
+
"locale"
|
|
380
|
+
])], 40, _hoisted_3)) : (openBlock(), createElementBlock("div", _hoisted_4, [createElementVNode("textarea", {
|
|
381
|
+
ref_key: "area",
|
|
382
|
+
ref: area,
|
|
383
|
+
class: "dsk-md__source",
|
|
384
|
+
value: markdown.value,
|
|
385
|
+
placeholder: __props.placeholder,
|
|
386
|
+
readonly: __props.readonly,
|
|
387
|
+
"aria-label": t.value.source,
|
|
388
|
+
spellcheck: "false",
|
|
389
|
+
onInput: onSource,
|
|
390
|
+
onKeydown: onTab
|
|
391
|
+
}, null, 40, _hoisted_5), mode.value === "split" ? (openBlock(), createElementBlock("div", {
|
|
392
|
+
key: 0,
|
|
393
|
+
class: "dsk-md__preview",
|
|
394
|
+
"aria-label": t.value.preview
|
|
395
|
+
}, [createVNode(unref(UidMarkdown), {
|
|
396
|
+
source: markdown.value,
|
|
397
|
+
"block-renderers": __props.blockRenderers,
|
|
398
|
+
"image-base": __props.imageBase,
|
|
399
|
+
"link-base": __props.linkBase
|
|
400
|
+
}, null, 8, [
|
|
401
|
+
"source",
|
|
402
|
+
"block-renderers",
|
|
403
|
+
"image-base",
|
|
404
|
+
"link-base"
|
|
405
|
+
])], 8, _hoisted_6)) : createCommentVNode("", true)]))], 6);
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
export { MarkdownEditor_default as MarkdownEditor, SOURCE_LANG, fromMarkdown, labelsFor, markdownEditorLocales, normalize, relativeTo, splitBlocks, toMarkdown };
|
|
410
|
+
|
|
411
|
+
//# sourceMappingURL=index.mjs.map
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* MarkdownEditor: the mode switch, the source and the preview; the visual mode is the WYSIWYG editor's. */
|
|
2
|
+
.dsk-md {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: var(--uid-space-sm, 8px);
|
|
6
|
+
min-width: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.dsk-md__modes {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-self: flex-end;
|
|
12
|
+
gap: 2px;
|
|
13
|
+
padding: 2px;
|
|
14
|
+
border: 1px solid var(--uid-color-border, #e5e7eb);
|
|
15
|
+
border-radius: var(--uid-radius-md, 8px);
|
|
16
|
+
background: var(--uid-color-bg-subtle, #f8fafc);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.dsk-md__mode {
|
|
20
|
+
padding: 3px 12px;
|
|
21
|
+
border: none;
|
|
22
|
+
border-radius: calc(var(--uid-radius-md, 8px) - 2px);
|
|
23
|
+
background: none;
|
|
24
|
+
color: var(--uid-color-text-secondary, #4b5563);
|
|
25
|
+
font: inherit;
|
|
26
|
+
font-size: var(--uid-font-size-sm, 13px);
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.dsk-md__mode:hover {
|
|
31
|
+
color: var(--uid-color-text, #111827);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.dsk-md__mode--on {
|
|
35
|
+
background: var(--uid-color-bg, #fff);
|
|
36
|
+
color: var(--uid-color-text, #111827);
|
|
37
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dsk-md__mode:focus-visible {
|
|
41
|
+
outline: 2px solid var(--uid-color-focus-ring, #3b82f6);
|
|
42
|
+
outline-offset: 1px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dsk-md__panes {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: 1fr;
|
|
48
|
+
gap: var(--uid-space-md, 12px);
|
|
49
|
+
min-height: var(--dsk-md-min-h, 240px);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dsk-md--split .dsk-md__panes {
|
|
53
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@media (max-width: 720px) {
|
|
57
|
+
.dsk-md--split .dsk-md__panes {
|
|
58
|
+
grid-template-columns: 1fr;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.dsk-md__source {
|
|
63
|
+
box-sizing: border-box;
|
|
64
|
+
width: 100%;
|
|
65
|
+
min-height: var(--dsk-md-min-h, 240px);
|
|
66
|
+
max-height: var(--dsk-md-max-h, none);
|
|
67
|
+
padding: 12px 14px;
|
|
68
|
+
border: 1px solid var(--uid-color-border, #e5e7eb);
|
|
69
|
+
border-radius: var(--uid-radius-md, 8px);
|
|
70
|
+
background: var(--uid-color-bg, #fff);
|
|
71
|
+
color: var(--uid-color-text, #111827);
|
|
72
|
+
font: 13px/1.6 var(--uid-font-family-mono, ui-monospace, monospace);
|
|
73
|
+
resize: vertical;
|
|
74
|
+
tab-size: 2;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dsk-md__source:focus-visible {
|
|
78
|
+
outline: 2px solid var(--uid-color-focus-ring, #3b82f6);
|
|
79
|
+
outline-offset: -1px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dsk-md__preview {
|
|
83
|
+
box-sizing: border-box;
|
|
84
|
+
min-height: var(--dsk-md-min-h, 240px);
|
|
85
|
+
max-height: var(--dsk-md-max-h, none);
|
|
86
|
+
overflow: auto;
|
|
87
|
+
padding: 4px 16px;
|
|
88
|
+
border: 1px solid var(--uid-color-border, #e5e7eb);
|
|
89
|
+
border-radius: var(--uid-radius-md, 8px);
|
|
90
|
+
background: var(--uid-color-bg, #fff);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* A block kept as written: the code block of the x-markdown language reads as such. */
|
|
94
|
+
.dsk-md__visual pre:has(> code.language-x-markdown) {
|
|
95
|
+
position: relative;
|
|
96
|
+
border-left: 3px solid var(--uid-color-warning, #d97706);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*$vite$:1*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dskripchenko/markdown-editor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A Markdown editor for Vue 3 that edits like a word processor: the value is Markdown, the editing is the @dskripchenko/wysiwyg engine — its toolbar, slash menu, shortcuts and live blocks — with source and side-by-side modes, and a lossless round trip",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"markdown-editor",
|
|
8
|
+
"wysiwyg",
|
|
9
|
+
"commonmark",
|
|
10
|
+
"gfm",
|
|
11
|
+
"vue",
|
|
12
|
+
"vue3",
|
|
13
|
+
"editor",
|
|
14
|
+
"rich-text"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/dskripchenko/markdown-editor#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/dskripchenko/markdown-editor.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/dskripchenko/markdown-editor/issues"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Denis Skripchenko (https://github.com/dskripchenko)",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"type": "module",
|
|
33
|
+
"main": "./dist/index.cjs",
|
|
34
|
+
"module": "./dist/index.mjs",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.mjs",
|
|
40
|
+
"require": "./dist/index.cjs"
|
|
41
|
+
},
|
|
42
|
+
"./style.css": "./dist/style.css",
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": [
|
|
46
|
+
"*.css"
|
|
47
|
+
],
|
|
48
|
+
"files": [
|
|
49
|
+
"dist",
|
|
50
|
+
"!dist/**/*.map",
|
|
51
|
+
"CHANGELOG.md"
|
|
52
|
+
],
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@dskripchenko/ui": "^1.19.1",
|
|
55
|
+
"@dskripchenko/wysiwyg": "^0.6.0",
|
|
56
|
+
"vue": "^3.4.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@changesets/cli": "^3.0.3",
|
|
60
|
+
"@dskripchenko/ui": "^1.19.1",
|
|
61
|
+
"@dskripchenko/wysiwyg": "^0.6.0",
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@microsoft/api-extractor": "^7.60.1",
|
|
64
|
+
"@types/node": "^22.20.5",
|
|
65
|
+
"@vitejs/plugin-vue": "^6.0.10",
|
|
66
|
+
"@vue/test-utils": "^2.5.1",
|
|
67
|
+
"eslint": "^10.14.0",
|
|
68
|
+
"jsdom": "^29.1.1",
|
|
69
|
+
"playwright": "1.64.1",
|
|
70
|
+
"prettier": "^3.9.10",
|
|
71
|
+
"typescript": "^6.0.3",
|
|
72
|
+
"typescript-eslint": "^8.72.1",
|
|
73
|
+
"vite": "^8.3.5",
|
|
74
|
+
"vite-plugin-dts": "^5.1.2",
|
|
75
|
+
"vitest": "^5.0.5",
|
|
76
|
+
"vue": "^3.5.44",
|
|
77
|
+
"vue-tsc": "^3.3.14"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "vue-tsc --noEmit -p tsconfig.json && vite build",
|
|
81
|
+
"test": "vitest run",
|
|
82
|
+
"test:watch": "vitest",
|
|
83
|
+
"lint": "eslint src tests scripts",
|
|
84
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
|
|
85
|
+
"changeset": "changeset",
|
|
86
|
+
"release": "pnpm build && changeset publish",
|
|
87
|
+
"check:browser": "node scripts/browser-check.mjs",
|
|
88
|
+
"dev": "vite --config playground/vite.config.ts"
|
|
89
|
+
}
|
|
6
90
|
}
|