sunny-html-editor 1.1.0 → 1.2.1

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.
Files changed (3) hide show
  1. package/README.md +20 -1
  2. package/html-editor.js +329 -0
  3. 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,8 @@ Excel/PDF出力に必要なライブラリは**使用時に自動で読み込ま
101
118
 
102
119
  ## バージョン履歴
103
120
 
121
+ - **1.2.1** - h1の「_」区切り改行対応
122
+ - **1.2.0** - Markdown変換機能追加(`data-markdown`属性対応)
104
123
  - **1.1.0** - ツールバー・コンテキストメニュー自動生成、外部ライブラリ遅延読み込み
105
124
  - **1.0.3** - nullチェック強化
106
125
  - **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, '&amp;')
437
+ .replace(/</g, '&lt;')
438
+ .replace(/>/g, '&gt;')
439
+ .replace(/"/g, '&quot;')
440
+ .replace(/'/g, '&#039;');
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sunny-html-editor",
3
- "version": "1.1.0",
4
- "description": "軽量WYSIWYGエディタ - 既存のHTMLに編集機能を後付けで追加",
3
+ "version": "1.2.1",
4
+ "description": "軽量WYSIWYGエディタ - 既存のHTMLに編集機能を後付けで追加、Markdown変換対応",
5
5
  "main": "html-editor.js",
6
6
  "files": [
7
7
  "html-editor.js",