sunny-html-editor 1.1.0 → 1.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/README.md +19 -1
- package/html-editor.js +329 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Sunny HTML Editor
|
|
2
2
|
|
|
3
|
-
軽量WYSIWYGエディタ。既存のHTMLに編集機能を後付けで追加できます。
|
|
3
|
+
軽量WYSIWYGエディタ。既存のHTMLに編集機能を後付けで追加できます。Markdown変換にも対応。
|
|
4
4
|
|
|
5
5
|
## 特徴
|
|
6
6
|
|
|
7
7
|
- **自己完結型** - ツールバー・コンテキストメニューを自動生成
|
|
8
|
+
- **Markdown変換** - `data-markdown`属性でMDテキストを自動変換
|
|
8
9
|
- **遅延読み込み** - Excel/PDF出力用ライブラリは使用時に自動読み込み
|
|
9
10
|
- **WYSIWYG編集** - 見たまま編集、右クリックで要素追加
|
|
10
11
|
- **キーボード操作** - ↑↓で要素間移動、Shift/Ctrl+Enterで項目追加
|
|
@@ -28,6 +29,8 @@ npm install sunny-html-editor
|
|
|
28
29
|
|
|
29
30
|
## 使い方
|
|
30
31
|
|
|
32
|
+
### 基本(HTMLコンテンツ)
|
|
33
|
+
|
|
31
34
|
`id="editorContainer"` を持つコンテナを用意するだけ。ツールバーとコンテキストメニューは自動生成されます。
|
|
32
35
|
|
|
33
36
|
```html
|
|
@@ -53,6 +56,20 @@ npm install sunny-html-editor
|
|
|
53
56
|
</html>
|
|
54
57
|
```
|
|
55
58
|
|
|
59
|
+
### Markdown変換モード
|
|
60
|
+
|
|
61
|
+
`data-markdown`属性にURLエンコードしたMarkdownテキストを設定すると、自動でHTMLに変換されます。
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<div class="container" id="editorContainer" data-markdown="URLエンコードされたMDテキスト"></div>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
JavaScript側で自動的に:
|
|
68
|
+
1. `data-markdown`属性をデコード
|
|
69
|
+
2. Markdown → HTML変換
|
|
70
|
+
3. コンテナ内にHTMLを生成
|
|
71
|
+
4. `data-markdown`属性を削除(HTML保存時に残らないように)
|
|
72
|
+
|
|
56
73
|
## 外部ライブラリ
|
|
57
74
|
|
|
58
75
|
Excel/PDF出力に必要なライブラリは**使用時に自動で読み込まれます**。事前の読み込みは不要です。
|
|
@@ -101,6 +118,7 @@ Excel/PDF出力に必要なライブラリは**使用時に自動で読み込ま
|
|
|
101
118
|
|
|
102
119
|
## バージョン履歴
|
|
103
120
|
|
|
121
|
+
- **1.2.0** - Markdown変換機能追加(`data-markdown`属性対応)
|
|
104
122
|
- **1.1.0** - ツールバー・コンテキストメニュー自動生成、外部ライブラリ遅延読み込み
|
|
105
123
|
- **1.0.3** - nullチェック強化
|
|
106
124
|
- **1.0.2** - DOMContentLoaded自動初期化
|
package/html-editor.js
CHANGED
|
@@ -125,11 +125,340 @@ async function ensureExcelLibrary() {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// ========== Markdown → HTML 変換 ==========
|
|
129
|
+
function convertMarkdownToHtml(md) {
|
|
130
|
+
var html = md;
|
|
131
|
+
var h2Counter = 0;
|
|
132
|
+
var h3Counter = 0;
|
|
133
|
+
var docTitle = '';
|
|
134
|
+
|
|
135
|
+
// コードブロックを一時退避
|
|
136
|
+
var codeBlocks = [];
|
|
137
|
+
html = html.replace(/```(\w*)[\r\n]+([\s\S]*?)```/g, function(match, lang, code) {
|
|
138
|
+
codeBlocks.push({lang: lang, code: mdTrim(code)});
|
|
139
|
+
return "\n\x00CODEBLOCK" + (codeBlocks.length - 1) + "\x00\n";
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// インラインコードを一時退避
|
|
143
|
+
var inlineCodes = [];
|
|
144
|
+
html = html.replace(/`([^`\r\n]+)`/g, function(match, code) {
|
|
145
|
+
inlineCodes.push(code);
|
|
146
|
+
return "\x00INLINE" + (inlineCodes.length - 1) + "\x00";
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// テーブルを一時退避
|
|
150
|
+
var tables = [];
|
|
151
|
+
html = html.replace(/(^\|.+\|$[\r\n]*)+/gm, function(tableMatch) {
|
|
152
|
+
var rows = mdTrim(tableMatch).split(/\r?\n/);
|
|
153
|
+
var validRows = [];
|
|
154
|
+
for (var i = 0; i < rows.length; i++) {
|
|
155
|
+
var r = mdTrim(rows[i]);
|
|
156
|
+
if (r) validRows.push(r);
|
|
157
|
+
}
|
|
158
|
+
if (validRows.length < 2) return tableMatch;
|
|
159
|
+
var separatorRow = validRows[1];
|
|
160
|
+
if (!/^\|[\s\-:|]+\|$/.test(separatorRow)) return tableMatch;
|
|
161
|
+
var headers = parseTableRow(validRows[0]);
|
|
162
|
+
var dataRows = [];
|
|
163
|
+
for (var i = 2; i < validRows.length; i++) {
|
|
164
|
+
dataRows.push(parseTableRow(validRows[i]));
|
|
165
|
+
}
|
|
166
|
+
tables.push({headers: headers, rows: dataRows});
|
|
167
|
+
return "\n\x00TABLE" + (tables.length - 1) + "\x00\n";
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 引用ブロックを一時退避
|
|
171
|
+
var blockquotes = [];
|
|
172
|
+
html = html.replace(/(^>.*(\r?\n|$))+/gm, function(match) {
|
|
173
|
+
var lines = match.split(/\r?\n/);
|
|
174
|
+
var content = [];
|
|
175
|
+
for (var i = 0; i < lines.length; i++) {
|
|
176
|
+
var line = lines[i].replace(/^>\s?/, '');
|
|
177
|
+
if (line) content.push(line);
|
|
178
|
+
}
|
|
179
|
+
blockquotes.push(content.join('\n'));
|
|
180
|
+
return "\n\x00QUOTE" + (blockquotes.length - 1) + "\x00\n";
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// 水平線
|
|
184
|
+
html = html.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '<hr>');
|
|
185
|
+
|
|
186
|
+
// 見出し変換
|
|
187
|
+
var lines = html.split(/\r?\n/);
|
|
188
|
+
for (var i = 0; i < lines.length; i++) {
|
|
189
|
+
var line = lines[i];
|
|
190
|
+
if (/^#\s+/.test(line) && !/^##/.test(line)) {
|
|
191
|
+
line = line.replace(/^#\s+(.+)$/, function(match, title) {
|
|
192
|
+
docTitle = title;
|
|
193
|
+
var displayTitle = title.replace(/\s*-\s*/g, '<br>');
|
|
194
|
+
return '<h1>' + displayTitle + '</h1>';
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else if (/^##\s+/.test(line) && !/^###/.test(line)) {
|
|
198
|
+
line = line.replace(/^##\s+(.+)$/, function(match, title) {
|
|
199
|
+
h2Counter++;
|
|
200
|
+
h3Counter = 0;
|
|
201
|
+
var sectionId = 'section' + h2Counter;
|
|
202
|
+
return '<h2 id="' + sectionId + '">' + title + '</h2>';
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else if (/^###\s+/.test(line) && !/^####/.test(line)) {
|
|
206
|
+
line = line.replace(/^###\s+(.+)$/, function(match, title) {
|
|
207
|
+
h3Counter++;
|
|
208
|
+
var sectionId = 'section' + h2Counter + '-' + h3Counter;
|
|
209
|
+
return '<h3 id="' + sectionId + '">' + title + '</h3>';
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (/^####\s+/.test(line) && !/^#####/.test(line)) {
|
|
213
|
+
line = line.replace(/^####\s+(.+)$/, '<h4>$1</h4>');
|
|
214
|
+
}
|
|
215
|
+
else if (/^#####\s+/.test(line)) {
|
|
216
|
+
line = line.replace(/^#####\s+(.+)$/, '<h5>$1</h5>');
|
|
217
|
+
}
|
|
218
|
+
lines[i] = line;
|
|
219
|
+
}
|
|
220
|
+
html = lines.join('\n');
|
|
221
|
+
|
|
222
|
+
// リスト変換
|
|
223
|
+
html = convertMdLists(html);
|
|
224
|
+
|
|
225
|
+
// インライン要素変換
|
|
226
|
+
html = convertMdInline(html);
|
|
227
|
+
|
|
228
|
+
// 段落変換
|
|
229
|
+
html = convertMdParagraphs(html);
|
|
230
|
+
|
|
231
|
+
// インラインコード復元
|
|
232
|
+
for (var i = 0; i < inlineCodes.length; i++) {
|
|
233
|
+
html = html.split("\x00INLINE" + i + "\x00").join('<code>' + escapeHtmlForMd(inlineCodes[i]) + '</code>');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// コードブロック復元
|
|
237
|
+
for (var i = 0; i < codeBlocks.length; i++) {
|
|
238
|
+
var lang = codeBlocks[i].lang ? ' class="language-' + codeBlocks[i].lang + '"' : '';
|
|
239
|
+
var codeHtml = '<pre><code' + lang + '>' + escapeHtmlForMd(codeBlocks[i].code) + '</code></pre>';
|
|
240
|
+
html = html.split("<p>\x00CODEBLOCK" + i + "\x00</p>").join(codeHtml);
|
|
241
|
+
html = html.split("\x00CODEBLOCK" + i + "\x00").join(codeHtml);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// テーブル復元
|
|
245
|
+
for (var i = 0; i < tables.length; i++) {
|
|
246
|
+
var t = tables[i];
|
|
247
|
+
var colCount = t.headers.length;
|
|
248
|
+
var tableClass = '';
|
|
249
|
+
if (colCount === 2) tableClass = ' class="table-2col"';
|
|
250
|
+
else if (colCount === 3) tableClass = ' class="table-3col"';
|
|
251
|
+
else tableClass = ' class="table-multi"';
|
|
252
|
+
|
|
253
|
+
var choiceColIndex = -1;
|
|
254
|
+
for (var j = 0; j < t.headers.length; j++) {
|
|
255
|
+
if (t.headers[j] === '選択肢') { choiceColIndex = j; break; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
var tableHtml = '<table' + tableClass + '>\n<tr>\n';
|
|
259
|
+
for (var j = 0; j < t.headers.length; j++) {
|
|
260
|
+
tableHtml += '<th>' + convertMdInline(escapeHtmlForMd(t.headers[j])) + '</th>\n';
|
|
261
|
+
}
|
|
262
|
+
tableHtml += '</tr>\n';
|
|
263
|
+
for (var j = 0; j < t.rows.length; j++) {
|
|
264
|
+
tableHtml += '<tr>\n';
|
|
265
|
+
for (var k = 0; k < t.rows[j].length; k++) {
|
|
266
|
+
var cellContent = convertMdInline(escapeHtmlForMd(t.rows[j][k]));
|
|
267
|
+
if (k === choiceColIndex) cellContent = cellContent.replace(/ \/ /g, '<br>');
|
|
268
|
+
tableHtml += '<td>' + cellContent + '</td>\n';
|
|
269
|
+
}
|
|
270
|
+
tableHtml += '</tr>\n';
|
|
271
|
+
}
|
|
272
|
+
tableHtml += '</table>';
|
|
273
|
+
html = html.split("<p>\x00TABLE" + i + "\x00</p>").join(tableHtml);
|
|
274
|
+
html = html.split("\x00TABLE" + i + "\x00").join(tableHtml);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 引用ブロック復元
|
|
278
|
+
for (var i = 0; i < blockquotes.length; i++) {
|
|
279
|
+
var quoteContent = convertMdInline(escapeHtmlForMd(blockquotes[i])).split('\n').join('<br>');
|
|
280
|
+
var quoteHtml = '<blockquote>' + quoteContent + '</blockquote>';
|
|
281
|
+
html = html.split("<p>\x00QUOTE" + i + "\x00</p>").join(quoteHtml);
|
|
282
|
+
html = html.split("\x00QUOTE" + i + "\x00").join(quoteHtml);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// インラインコードの再復元
|
|
286
|
+
for (var i = 0; i < inlineCodes.length; i++) {
|
|
287
|
+
html = html.split("\x00INLINE" + i + "\x00").join('<code>' + escapeHtmlForMd(inlineCodes[i]) + '</code>');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { html: html, title: docTitle };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function convertMdLists(html) {
|
|
294
|
+
var lines = html.split(/\r?\n/);
|
|
295
|
+
var result = [];
|
|
296
|
+
var i = 0;
|
|
297
|
+
while (i < lines.length) {
|
|
298
|
+
var line = lines[i];
|
|
299
|
+
if (/^(\s*)\d+\.\s+/.test(line)) {
|
|
300
|
+
var listResult = parseMdNestedList(lines, i, 'ol');
|
|
301
|
+
result.push(listResult.html);
|
|
302
|
+
i = listResult.nextIndex;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (/^(\s*)[\-\*]\s+/.test(line)) {
|
|
306
|
+
var listResult = parseMdNestedList(lines, i, 'ul');
|
|
307
|
+
result.push(listResult.html);
|
|
308
|
+
i = listResult.nextIndex;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
result.push(line);
|
|
312
|
+
i++;
|
|
313
|
+
}
|
|
314
|
+
return result.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseMdNestedList(lines, startIndex, listType) {
|
|
318
|
+
var result = [];
|
|
319
|
+
var i = startIndex;
|
|
320
|
+
var baseIndent = getMdIndent(lines[startIndex]);
|
|
321
|
+
result.push('<' + listType + '>');
|
|
322
|
+
|
|
323
|
+
while (i < lines.length) {
|
|
324
|
+
var line = lines[i];
|
|
325
|
+
var currentIndent = getMdIndent(line);
|
|
326
|
+
var trimmedLine = mdTrim(line);
|
|
327
|
+
|
|
328
|
+
if (trimmedLine === '') break;
|
|
329
|
+
if (currentIndent < baseIndent && trimmedLine !== '') break;
|
|
330
|
+
|
|
331
|
+
if (currentIndent === baseIndent) {
|
|
332
|
+
var itemMatch = trimmedLine.match(/^[\-\*]\s+(.+)$/) || trimmedLine.match(/^\d+\.\s+(.+)$/);
|
|
333
|
+
if (itemMatch) {
|
|
334
|
+
var hasNested = false;
|
|
335
|
+
if (i + 1 < lines.length) {
|
|
336
|
+
var nextIndent = getMdIndent(lines[i + 1]);
|
|
337
|
+
var nextTrimmed = mdTrim(lines[i + 1]);
|
|
338
|
+
if (nextIndent > currentIndent && (nextTrimmed.match(/^[\-\*]\s+/) || nextTrimmed.match(/^\d+\.\s+/))) {
|
|
339
|
+
hasNested = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (hasNested) {
|
|
343
|
+
result.push('<li>' + itemMatch[1]);
|
|
344
|
+
var nestedType = mdTrim(lines[i + 1]).match(/^\d+\.\s+/) ? 'ol' : 'ul';
|
|
345
|
+
var nestedResult = parseMdNestedList(lines, i + 1, nestedType);
|
|
346
|
+
result.push(nestedResult.html);
|
|
347
|
+
result.push('</li>');
|
|
348
|
+
i = nestedResult.nextIndex;
|
|
349
|
+
continue;
|
|
350
|
+
} else {
|
|
351
|
+
result.push('<li>' + itemMatch[1] + '</li>');
|
|
352
|
+
}
|
|
353
|
+
} else { break; }
|
|
354
|
+
}
|
|
355
|
+
else if (currentIndent > baseIndent) {
|
|
356
|
+
var nestedType = trimmedLine.match(/^\d+\.\s+/) ? 'ol' : 'ul';
|
|
357
|
+
var nestedResult = parseMdNestedList(lines, i, nestedType);
|
|
358
|
+
result.push(nestedResult.html);
|
|
359
|
+
i = nestedResult.nextIndex;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
i++;
|
|
363
|
+
}
|
|
364
|
+
result.push('</' + listType + '>');
|
|
365
|
+
return { html: result.join('\n'), nextIndex: i };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function convertMdInline(text) {
|
|
369
|
+
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
370
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
371
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
372
|
+
text = text.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
373
|
+
text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
|
374
|
+
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
375
|
+
return text;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function convertMdParagraphs(html) {
|
|
379
|
+
var lines = html.split(/\r?\n/);
|
|
380
|
+
var result = [];
|
|
381
|
+
var para = [];
|
|
382
|
+
for (var i = 0; i < lines.length; i++) {
|
|
383
|
+
var line = mdTrim(lines[i]);
|
|
384
|
+
if (line === '') {
|
|
385
|
+
if (para.length > 0) {
|
|
386
|
+
result.push(wrapMdParagraph(para.join(' ')));
|
|
387
|
+
para = [];
|
|
388
|
+
}
|
|
389
|
+
} else if (isMdBlockElement(line)) {
|
|
390
|
+
if (para.length > 0) {
|
|
391
|
+
result.push(wrapMdParagraph(para.join(' ')));
|
|
392
|
+
para = [];
|
|
393
|
+
}
|
|
394
|
+
result.push(line);
|
|
395
|
+
} else {
|
|
396
|
+
para.push(line);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (para.length > 0) {
|
|
400
|
+
result.push(wrapMdParagraph(para.join(' ')));
|
|
401
|
+
}
|
|
402
|
+
return result.join('\n');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isMdBlockElement(line) {
|
|
406
|
+
return /^<(h[1-5]|ul|ol|li|\/ul|\/ol|hr|table|blockquote|pre)/.test(line) ||
|
|
407
|
+
/^\x00(CODEBLOCK|TABLE|QUOTE)\d+\x00$/.test(line);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function wrapMdParagraph(content) {
|
|
411
|
+
if (/^<(h[1-5]|ul|ol|li|hr|table|blockquote|pre)/.test(content)) return content;
|
|
412
|
+
if (/^\x00(CODEBLOCK|TABLE|QUOTE)\d+\x00$/.test(content)) return content;
|
|
413
|
+
return '<p>' + content + '</p>';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function parseTableRow(row) {
|
|
417
|
+
var cells = row.split('|');
|
|
418
|
+
var result = [];
|
|
419
|
+
for (var i = 1; i < cells.length - 1; i++) {
|
|
420
|
+
result.push(mdTrim(cells[i]));
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function getMdIndent(line) {
|
|
426
|
+
var match = line.match(/^(\s*)/);
|
|
427
|
+
return match ? match[1].length : 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function mdTrim(str) {
|
|
431
|
+
return str.replace(/^\s+|\s+$/g, '');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function escapeHtmlForMd(text) {
|
|
435
|
+
return text
|
|
436
|
+
.replace(/&/g, '&')
|
|
437
|
+
.replace(/</g, '<')
|
|
438
|
+
.replace(/>/g, '>')
|
|
439
|
+
.replace(/"/g, '"')
|
|
440
|
+
.replace(/'/g, ''');
|
|
441
|
+
}
|
|
442
|
+
|
|
128
443
|
// ========== 初期化 ==========
|
|
129
444
|
document.addEventListener('DOMContentLoaded', function() {
|
|
130
445
|
var container = document.getElementById('editorContainer');
|
|
131
446
|
if (!container) return;
|
|
132
447
|
|
|
448
|
+
// data-markdown属性があればMD→HTML変換
|
|
449
|
+
var markdownData = container.dataset.markdown;
|
|
450
|
+
if (markdownData) {
|
|
451
|
+
var decoded = decodeURIComponent(markdownData);
|
|
452
|
+
var result = convertMarkdownToHtml(decoded);
|
|
453
|
+
container.innerHTML = result.html;
|
|
454
|
+
// タイトル更新
|
|
455
|
+
if (result.title) {
|
|
456
|
+
document.title = result.title;
|
|
457
|
+
}
|
|
458
|
+
// 変換後は属性を削除(HTML保存時に巨大な属性が残らないように)
|
|
459
|
+
container.removeAttribute('data-markdown');
|
|
460
|
+
}
|
|
461
|
+
|
|
133
462
|
// ツールバー挿入
|
|
134
463
|
container.insertAdjacentHTML('afterbegin', getToolbarHtml());
|
|
135
464
|
|
package/package.json
CHANGED