sunny-html-editor 1.2.5 → 1.3.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.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## バージョン履歴
6
6
 
7
+ - **1.3.0** - Mermaid図表示対応、Excel出力にMermaid画像埋め込み、テーブル全セルで` / `改行対応、長文字列のword-break対応、PDF出力機能削除
7
8
  - **1.2.5** - サイドバー開閉時の横スクロールバー問題を修正
8
9
  - **1.2.4** - h1改行を最初の「-」「_」のみに限定
9
10
  - **1.2.3** - サイドバーh1の改行(br)維持を修正
package/html-editor.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Sunny HTML Editor - スタイルシート
3
- * @version 1.0.0
3
+ * @version 1.3.0
4
4
  */
5
5
 
6
6
  * {
@@ -189,6 +189,7 @@ th, td {
189
189
  padding: 10px;
190
190
  text-align: left;
191
191
  vertical-align: top;
192
+ word-break: break-all;
192
193
  }
193
194
  th {
194
195
  background: linear-gradient(180deg, #f8f8f8 0%, #efefef 100%);
@@ -389,3 +390,18 @@ del {
389
390
  .sidebar {
390
391
  transition: transform 0.3s ease;
391
392
  }
393
+
394
+ /* Mermaid図 */
395
+ .mermaid-container {
396
+ margin: 15px 0 15px 20px;
397
+ padding: 15px;
398
+ background: #fafafa;
399
+ border: 1px solid #e0e0e0;
400
+ border-radius: 4px;
401
+ overflow-x: auto;
402
+ text-align: center;
403
+ }
404
+ .mermaid-container svg {
405
+ max-width: 100%;
406
+ height: auto;
407
+ }
package/html-editor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Sunny HTML Editor - 軽量WYSIWYGエディタ
3
- * @version 1.2.5
3
+ * @version 1.3.1
4
4
  */
5
5
 
6
6
  // ========== HTML生成関数 ==========
@@ -12,7 +12,6 @@ function getToolbarHtml() {
12
12
  '<button id="exportHtml" title="HTMLコピー">🌐</button>' +
13
13
  '<button id="saveHtml" title="HTML保存">💾</button>' +
14
14
  '<button id="exportExcel" title="Excel出力">📊</button>' +
15
- '<button id="exportPdf" title="PDF出力">📕</button>' +
16
15
  '</div>';
17
16
  }
18
17
 
@@ -110,18 +109,43 @@ function loadScript(src) {
110
109
  return libraryPromises[src];
111
110
  }
112
111
 
113
- async function ensurePdfLibraries() {
114
- if (!window.jspdf) {
115
- await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
112
+ async function ensureExcelLibrary() {
113
+ if (!window.ExcelJS) {
114
+ await loadScript('https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js');
116
115
  }
117
- if (!window.html2canvas) {
118
- await loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js');
116
+ }
117
+
118
+ async function ensureMermaidLibrary() {
119
+ if (!window.mermaid) {
120
+ await loadScript('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js');
121
+ window.mermaid.initialize({ startOnLoad: false, theme: 'default' });
119
122
  }
120
123
  }
121
124
 
122
- async function ensureExcelLibrary() {
123
- if (!window.ExcelJS) {
124
- await loadScript('https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js');
125
+ async function renderMermaidBlocks(root) {
126
+ var codeBlocks = root.querySelectorAll('pre > code.language-mermaid');
127
+ if (codeBlocks.length === 0) return;
128
+
129
+ await ensureMermaidLibrary();
130
+
131
+ for (var i = 0; i < codeBlocks.length; i++) {
132
+ var code = codeBlocks[i];
133
+ var pre = code.parentElement;
134
+ var mermaidText = code.textContent;
135
+
136
+ var wrapper = document.createElement('div');
137
+ wrapper.className = 'mermaid-container';
138
+ wrapper.dataset.mermaidSource = mermaidText;
139
+
140
+ try {
141
+ var id = 'mermaid-' + Date.now() + '-' + i;
142
+ var result = await window.mermaid.render(id, mermaidText);
143
+ wrapper.innerHTML = result.svg;
144
+ } catch (err) {
145
+ wrapper.innerHTML = '<pre style="color:#c00;border:1px solid #c00;padding:10px;border-radius:4px;">Mermaid描画エラー: ' + err.message + '</pre>';
146
+ }
147
+
148
+ pre.replaceWith(wrapper);
125
149
  }
126
150
  }
127
151
 
@@ -250,11 +274,6 @@ function convertMarkdownToHtml(md) {
250
274
  else if (colCount === 3) tableClass = ' class="table-3col"';
251
275
  else tableClass = ' class="table-multi"';
252
276
 
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
277
  var tableHtml = '<table' + tableClass + '>\n<tr>\n';
259
278
  for (var j = 0; j < t.headers.length; j++) {
260
279
  tableHtml += '<th>' + convertMdInline(escapeHtmlForMd(t.headers[j])) + '</th>\n';
@@ -264,7 +283,7 @@ function convertMarkdownToHtml(md) {
264
283
  tableHtml += '<tr>\n';
265
284
  for (var k = 0; k < t.rows[j].length; k++) {
266
285
  var cellContent = convertMdInline(escapeHtmlForMd(t.rows[j][k]));
267
- if (k === choiceColIndex) cellContent = cellContent.replace(/ \/ /g, '<br>');
286
+ cellContent = cellContent.replace(/ \/ /g, '<br>');
268
287
  tableHtml += '<td>' + cellContent + '</td>\n';
269
288
  }
270
289
  tableHtml += '</tr>\n';
@@ -462,6 +481,9 @@ document.addEventListener('DOMContentLoaded', function() {
462
481
  // ツールバー挿入
463
482
  container.insertAdjacentHTML('afterbegin', getToolbarHtml());
464
483
 
484
+ // Mermaidブロックを描画
485
+ renderMermaidBlocks(container);
486
+
465
487
  // コンテキストメニュー挿入(bodyの末尾に配置)
466
488
  document.body.insertAdjacentHTML('beforeend', getContextMenusHtml());
467
489
 
@@ -472,7 +494,6 @@ document.addEventListener('DOMContentLoaded', function() {
472
494
  var exportMdBtn = document.getElementById('exportMd');
473
495
  var exportHtmlBtn = document.getElementById('exportHtml');
474
496
  var exportExcelBtn = document.getElementById('exportExcel');
475
- var exportPdfBtn = document.getElementById('exportPdf');
476
497
  var isEditMode = false;
477
498
  var sectionCounter = { h2: 100, h3: 100, h4: 100 };
478
499
 
@@ -502,41 +523,43 @@ document.addEventListener('DOMContentLoaded', function() {
502
523
  }, 10);
503
524
  }
504
525
 
526
+ // ========== Mermaidコンテナ復元ヘルパー ==========
527
+ function restoreMermaidToPre(root) {
528
+ root.querySelectorAll('.mermaid-container').forEach(function(container) {
529
+ var source = container.dataset.mermaidSource;
530
+ if (source) {
531
+ var pre = document.createElement('pre');
532
+ var code = document.createElement('code');
533
+ code.className = 'language-mermaid';
534
+ code.textContent = source;
535
+ pre.appendChild(code);
536
+ container.replaceWith(pre);
537
+ }
538
+ });
539
+ }
540
+
505
541
  // ========== HTML保存 ==========
542
+ var EDITOR_CDN_BASE = 'https://cdn.jsdelivr.net/npm/sunny-html-editor@1.3.1';
543
+
506
544
  if (saveHtmlBtn) saveHtmlBtn.addEventListener('click', function() {
507
545
  var wasEditMode = isEditMode;
508
546
  if (isEditMode && editModeBtn) editModeBtn.click();
509
547
 
510
- // ツールバーとコンテキストメニューを一時的に非表示
511
- var toolbar = container.querySelector('.toolbar');
512
- var contextMenus = document.querySelectorAll('.context-menu');
513
-
514
- if (toolbar) toolbar.style.display = 'none';
515
- contextMenus.forEach(function(menu) { menu.style.display = 'none'; });
516
-
517
- // HTML取得(ツールバーとコンテキストメニューを除外したクリーンなHTML)
518
- var docClone = document.documentElement.cloneNode(true);
519
- var cloneToolbar = docClone.querySelector('.toolbar');
520
- var cloneMenus = docClone.querySelectorAll('.context-menu');
548
+ // コンテンツHTML取得(クリーン版)
549
+ var contentClone = container.cloneNode(true);
550
+ var cloneToolbar = contentClone.querySelector('.toolbar');
551
+ var cloneMenus = contentClone.querySelectorAll('.context-menu');
521
552
  if (cloneToolbar) cloneToolbar.remove();
522
553
  cloneMenus.forEach(function(menu) { menu.remove(); });
554
+ restoreMermaidToPre(contentClone);
555
+ var contentHtml = contentClone.innerHTML;
523
556
 
524
- var html = '<!DOCTYPE html>\n' + docClone.outerHTML;
525
-
526
- // 表示を復元
527
- if (toolbar) toolbar.style.display = '';
528
- contextMenus.forEach(function(menu) { menu.style.display = ''; });
529
-
530
- if (wasEditMode && editModeBtn) editModeBtn.click();
531
-
532
- var blob = new Blob([html], { type: 'text/html; charset=utf-8' });
533
- var url = URL.createObjectURL(blob);
534
- var a = document.createElement('a');
535
- a.href = url;
536
-
557
+ // タイトル・ファイル名取得
537
558
  var h1Element = container.querySelector('h1');
559
+ var title = 'Design Spec';
538
560
  var fileName = 'design_spec';
539
561
  if (h1Element) {
562
+ title = h1Element.textContent.trim();
540
563
  fileName = h1Element.innerHTML
541
564
  .replace(/<br\s*\/?>/gi, '-')
542
565
  .replace(/<\/p>\s*<p>/gi, '-')
@@ -544,9 +567,36 @@ document.addEventListener('DOMContentLoaded', function() {
544
567
  .replace(/\s+/g, '')
545
568
  .trim();
546
569
  }
570
+
571
+ // CDN参照の単一HTMLファイル組み立て
572
+ var html = '<!DOCTYPE html>\n' +
573
+ '<html lang="ja">\n' +
574
+ '<head>\n' +
575
+ '<meta charset="UTF-8">\n' +
576
+ '<title>' + title.replace(/</g, '&lt;') + '</title>\n' +
577
+ '<link rel="stylesheet" href="' + EDITOR_CDN_BASE + '/html-editor.css">\n' +
578
+ '</head>\n' +
579
+ '<body>\n' +
580
+ '<div class="layout">\n' +
581
+ ' <main class="main">\n' +
582
+ ' <div class="container" id="editorContainer">\n' +
583
+ contentHtml + '\n' +
584
+ ' </div>\n' +
585
+ ' </main>\n' +
586
+ '</div>\n' +
587
+ '<script src="' + EDITOR_CDN_BASE + '/html-editor.js"><\/script>\n' +
588
+ '</body>\n' +
589
+ '</html>';
590
+
591
+ var blob = new Blob([html], { type: 'text/html; charset=utf-8' });
592
+ var url = URL.createObjectURL(blob);
593
+ var a = document.createElement('a');
594
+ a.href = url;
547
595
  a.download = fileName + '.html';
548
596
  a.click();
549
597
  URL.revokeObjectURL(url);
598
+
599
+ if (wasEditMode && editModeBtn) editModeBtn.click();
550
600
  });
551
601
 
552
602
  // ========== Markdown出力 ==========
@@ -595,6 +645,9 @@ document.addEventListener('DOMContentLoaded', function() {
595
645
  // edit-modeクラスを除去
596
646
  content.classList.remove('edit-mode');
597
647
 
648
+ // Mermaidコンテナをpre/codeに復元
649
+ restoreMermaidToPre(content);
650
+
598
651
  // id属性を除去(containerのid)
599
652
  content.removeAttribute('id');
600
653
 
@@ -726,7 +779,7 @@ document.addEventListener('DOMContentLoaded', function() {
726
779
  if (!content) content = container;
727
780
 
728
781
  // 対象要素を拡張(ul, ol, pre, blockquote を追加)
729
- var elements = content.querySelectorAll('h1, h2, h3, h4, p, table, hr, ul, ol, pre, blockquote');
782
+ var elements = content.querySelectorAll('h1, h2, h3, h4, p, table, hr, ul, ol, pre, blockquote, .mermaid-container');
730
783
 
731
784
  // 処理済みの要素を追跡(ネストされたリストの重複処理を防ぐ)
732
785
  var processed = new Set();
@@ -779,34 +832,26 @@ document.addEventListener('DOMContentLoaded', function() {
779
832
  lines = lines.concat(bqLines);
780
833
  lines.push('');
781
834
  processed.add(el);
835
+ } else if (el.classList && el.classList.contains('mermaid-container')) {
836
+ var mermaidSource = el.dataset.mermaidSource;
837
+ if (mermaidSource) {
838
+ lines.push('```mermaid');
839
+ lines.push(mermaidSource);
840
+ lines.push('```');
841
+ lines.push('');
842
+ }
843
+ processed.add(el);
782
844
  } else if (tag === 'TABLE') {
783
845
  var rows = el.querySelectorAll('tr');
784
846
 
785
- // ヘッダー行から「選択肢」列のインデックスを特定
786
- var choiceColIndex = -1;
787
- if (rows.length > 0) {
788
- var headerCells = rows[0].querySelectorAll('th, td');
789
- headerCells.forEach(function(cell, index) {
790
- if (cell.textContent.trim() === '選択肢') {
791
- choiceColIndex = index;
792
- }
793
- });
794
- }
795
-
796
847
  rows.forEach(function(row, rowIndex) {
797
848
  var cells = row.querySelectorAll('th, td');
798
849
  var cellTexts = [];
799
850
  cells.forEach(function(cell, cellIndex) {
800
- var text;
801
- // 「選択肢」列かつデータ行の場合は<br>を / に変換
802
- if (cellIndex === choiceColIndex && rowIndex > 0) {
803
- var tempDiv = document.createElement('div');
804
- tempDiv.innerHTML = cell.innerHTML;
805
- tempDiv.innerHTML = tempDiv.innerHTML.replace(/<br\s*\/?>/gi, ' / ');
806
- text = tempDiv.textContent.trim();
807
- } else {
808
- text = cell.textContent.trim();
809
- }
851
+ var tempDiv = document.createElement('div');
852
+ tempDiv.innerHTML = cell.innerHTML;
853
+ tempDiv.innerHTML = tempDiv.innerHTML.replace(/<br\s*\/?>/gi, ' / ');
854
+ var text = tempDiv.textContent.trim();
810
855
  cellTexts.push(text);
811
856
  });
812
857
  lines.push('| ' + cellTexts.join(' | ') + ' |');
@@ -1829,222 +1874,156 @@ document.addEventListener('DOMContentLoaded', function() {
1829
1874
  }
1830
1875
  }
1831
1876
 
1832
- // ========== PDF出力 ==========
1833
- if (exportPdfBtn) exportPdfBtn.addEventListener('click', async function() {
1834
- var wasEditMode = isEditMode;
1835
- if (isEditMode && editModeBtn) editModeBtn.click();
1836
-
1837
- exportPdfBtn.textContent = '...';
1838
-
1839
- try {
1840
- // ライブラリ遅延読み込み
1841
- await ensurePdfLibraries();
1842
-
1843
- // jsPDFインスタンス作成
1844
- var { jsPDF } = window.jspdf;
1845
- var pdf = new jsPDF({
1846
- unit: 'mm',
1847
- format: 'a4',
1848
- orientation: 'portrait'
1849
- });
1850
-
1851
- var pageWidth = 210;
1852
- var pageHeight = 297;
1853
- var margin = 15;
1854
- var contentWidth = pageWidth - margin * 2;
1855
- var contentHeight = pageHeight - margin * 2;
1856
- var currentY = margin;
1857
- var currentPage = 1;
1858
-
1859
- // しおり情報
1860
- var bookmarks = [];
1861
-
1862
- // コンテンツを複製してPDF用に整形
1863
- var content = container.cloneNode(true);
1864
-
1865
- // ツールバーと右クリックメニューを除去
1866
- var toolbar = content.querySelector('.toolbar');
1867
- if (toolbar) toolbar.remove();
1868
- content.querySelectorAll('.context-menu').forEach(function(menu) {
1869
- menu.remove();
1870
- });
1871
- content.querySelectorAll('[contenteditable]').forEach(function(el) {
1872
- el.removeAttribute('contenteditable');
1873
- });
1874
-
1875
- // 一時的にDOMに追加(html2canvas用)
1876
- content.style.position = 'absolute';
1877
- content.style.left = '-9999px';
1878
- content.style.width = contentWidth * 3.78 + 'px'; // mm to px
1879
- content.style.background = 'white';
1880
- content.style.padding = '0';
1881
- document.body.appendChild(content);
1882
-
1883
- // h2で分割してセクションごとに処理
1884
- var allElements = content.querySelectorAll('h1, h2, h3, h4, p, ul, ol, pre, blockquote, table, hr');
1885
- var sections = [];
1886
- var currentSection = { elements: [], h2Text: null, h3List: [] };
1887
-
1888
- allElements.forEach(function(el) {
1889
- // ネストされたリストはスキップ(親リストで処理される)
1890
- if ((el.tagName === 'UL' || el.tagName === 'OL') && el.parentElement.closest('ul, ol')) {
1891
- return;
1892
- }
1893
-
1894
- if (el.tagName === 'H2') {
1895
- if (currentSection.elements.length > 0 || currentSection.h2Text) {
1896
- sections.push(currentSection);
1897
- }
1898
- currentSection = { elements: [el], h2Text: el.textContent.trim(), h3List: [] };
1899
- } else if (el.tagName === 'H3') {
1900
- currentSection.elements.push(el);
1901
- currentSection.h3List.push({ text: el.textContent.trim(), pageNum: null });
1902
- } else {
1903
- currentSection.elements.push(el);
1904
- }
1905
- });
1906
- if (currentSection.elements.length > 0) {
1907
- sections.push(currentSection);
1908
- }
1909
-
1910
- // 各セクションを処理
1911
- for (var i = 0; i < sections.length; i++) {
1912
- var section = sections[i];
1913
-
1914
- // セクション内の要素を一つのdivにまとめる
1915
- var sectionDiv = document.createElement('div');
1916
- sectionDiv.style.background = 'white';
1917
- sectionDiv.style.padding = '10px';
1918
- section.elements.forEach(function(el) {
1919
- sectionDiv.appendChild(el.cloneNode(true));
1920
- });
1921
- content.innerHTML = '';
1922
- content.appendChild(sectionDiv);
1923
-
1924
- // html2canvasでキャプチャ
1925
- var canvas = await html2canvas(sectionDiv, {
1926
- scale: 2,
1927
- useCORS: true,
1928
- backgroundColor: '#ffffff'
1929
- });
1930
-
1931
- var imgData = canvas.toDataURL('image/jpeg', 0.95);
1932
- var imgWidth = contentWidth;
1933
- var imgHeight = (canvas.height * imgWidth) / canvas.width;
1934
-
1935
- // ページに収まるかチェック
1936
- if (currentY + imgHeight > pageHeight - margin) {
1937
- // 新しいページを追加
1938
- pdf.addPage();
1939
- currentPage++;
1940
- currentY = margin;
1941
- }
1877
+
1878
+ // ========== Excel出力 ==========
1879
+ var fontSizes = {
1880
+ heading: 14,
1881
+ tableHeader: 13,
1882
+ body: 12
1883
+ };
1884
+
1885
+ function svgToPngBase64(svgElement) {
1886
+ return new Promise(function(resolve, reject) {
1887
+ try {
1888
+ // SVGの実サイズを取得
1889
+ var rect = svgElement.getBoundingClientRect();
1890
+ var svgWidth = rect.width || parseInt(svgElement.getAttribute('width')) || 800;
1891
+ var svgHeight = rect.height || parseInt(svgElement.getAttribute('height')) || 400;
1942
1892
 
1943
- // セクションのh2をしおりに追加(ページ確定後)
1944
- if (section.h2Text) {
1945
- bookmarks.push({
1946
- text: section.h2Text,
1947
- page: currentPage,
1948
- level: 1,
1949
- children: []
1950
- });
1893
+ // SVGクローンにサイズを明示設定(Image読み込み時に必要)
1894
+ var cloneSvg = svgElement.cloneNode(true);
1895
+ cloneSvg.setAttribute('width', svgWidth);
1896
+ cloneSvg.setAttribute('height', svgHeight);
1897
+ if (!cloneSvg.getAttribute('xmlns')) {
1898
+ cloneSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
1951
1899
  }
1952
1900
 
1953
- // h3のページ番号を記録
1954
- if (section.h3List.length > 0 && bookmarks.length > 0) {
1955
- var parentBookmark = bookmarks[bookmarks.length - 1];
1956
- section.h3List.forEach(function(h3) {
1957
- parentBookmark.children.push({
1958
- text: h3.text,
1959
- page: currentPage,
1960
- level: 2
1961
- });
1962
- });
1963
- }
1901
+ var svgData = new XMLSerializer().serializeToString(cloneSvg);
1902
+ var svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
1903
+ var url = URL.createObjectURL(svgBlob);
1964
1904
 
1965
- // 画像が大きすぎる場合は分割
1966
- if (imgHeight > contentHeight) {
1967
- var remainingHeight = imgHeight;
1968
- var sourceY = 0;
1905
+ var img = new Image();
1906
+ img.onload = function() {
1907
+ var scale = 2;
1908
+ var w = img.naturalWidth || svgWidth;
1909
+ var h = img.naturalHeight || svgHeight;
1910
+ var canvas = document.createElement('canvas');
1911
+ canvas.width = w * scale;
1912
+ canvas.height = h * scale;
1913
+ var ctx = canvas.getContext('2d');
1914
+ ctx.fillStyle = '#ffffff';
1915
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1916
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
1969
1917
 
1970
- while (remainingHeight > 0) {
1971
- var drawHeight = Math.min(contentHeight - (currentY - margin), remainingHeight);
1972
- var drawHeightPx = (drawHeight / imgHeight) * canvas.height;
1973
-
1974
- // キャンバスの一部を切り出し
1975
- var partCanvas = document.createElement('canvas');
1976
- partCanvas.width = canvas.width;
1977
- partCanvas.height = drawHeightPx;
1978
- var ctx = partCanvas.getContext('2d');
1979
- ctx.drawImage(canvas, 0, sourceY, canvas.width, drawHeightPx, 0, 0, canvas.width, drawHeightPx);
1980
-
1981
- var partImgData = partCanvas.toDataURL('image/jpeg', 0.95);
1982
- pdf.addImage(partImgData, 'JPEG', margin, currentY, imgWidth, drawHeight);
1983
-
1984
- sourceY += drawHeightPx;
1985
- remainingHeight -= drawHeight;
1986
-
1987
- if (remainingHeight > 0) {
1988
- pdf.addPage();
1989
- currentPage++;
1990
- currentY = margin;
1991
- } else {
1992
- currentY += drawHeight + 5;
1993
- }
1994
- }
1995
- } else {
1996
- pdf.addImage(imgData, 'JPEG', margin, currentY, imgWidth, imgHeight);
1997
- currentY += imgHeight + 5;
1998
- }
1918
+ URL.revokeObjectURL(url);
1919
+
1920
+ var dataUrl = canvas.toDataURL('image/png');
1921
+ var base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
1922
+ resolve(base64);
1923
+ };
1924
+ img.onerror = function() {
1925
+ URL.revokeObjectURL(url);
1926
+ reject(new Error('SVG→PNG変換に失敗'));
1927
+ };
1928
+ img.src = url;
1929
+ } catch (err) {
1930
+ reject(err);
1999
1931
  }
1932
+ });
1933
+ }
1934
+
1935
+ // Excel用のデータシリアライズ(Chrome拡張sandbox用)
1936
+ async function serializeContentForExcel(sections, fileName) {
1937
+ var data = { fileName: fileName, sections: [] };
1938
+
1939
+ for (var s = 0; s < sections.length; s++) {
1940
+ var section = sections[s];
1941
+ var sectionData = { title: section.title, elements: [] };
2000
1942
 
2001
- // 一時要素を削除
2002
- document.body.removeChild(content);
2003
-
2004
- // しおりを追加
2005
- bookmarks.forEach(function(bm) {
2006
- var parent = pdf.outline.add(null, bm.text, { pageNumber: bm.page });
2007
- bm.children.forEach(function(child) {
2008
- pdf.outline.add(parent, child.text, { pageNumber: child.page });
2009
- });
2010
- });
2011
-
2012
- // タイトル取得
2013
- var h1 = container.querySelector('h1');
2014
- var fileName = h1 ? h1.textContent.trim().replace(/[\\/:*?"<>|]/g, '_') : 'document';
2015
-
2016
- // PDF保存
2017
- pdf.save(fileName + '.pdf');
1943
+ for (var e = 0; e < section.elements.length; e++) {
1944
+ var elData = await serializeElement(section.elements[e]);
1945
+ if (elData) sectionData.elements.push(elData);
1946
+ }
2018
1947
 
2019
- } catch (error) {
2020
- console.error('PDF生成エラー:', error);
2021
- alert('PDF生成中にエラーが発生しました: ' + error.message);
1948
+ data.sections.push(sectionData);
2022
1949
  }
2023
1950
 
2024
- exportPdfBtn.textContent = '📕';
2025
- if (wasEditMode && editModeBtn) editModeBtn.click();
2026
- });
1951
+ return data;
1952
+ }
2027
1953
 
2028
- // ========== Excel出力 ==========
2029
- var fontSizes = {
2030
- heading: 14,
2031
- tableHeader: 13,
2032
- body: 12
2033
- };
1954
+ async function serializeElement(element) {
1955
+ if (element.classList && element.classList.contains('mermaid-container')) {
1956
+ var svg = element.querySelector('svg');
1957
+ if (svg) {
1958
+ try {
1959
+ var pngBase64 = await svgToPngBase64(svg);
1960
+ var svgRect = svg.getBoundingClientRect();
1961
+ return {
1962
+ tag: 'MERMAID', pngBase64: pngBase64,
1963
+ width: svgRect.width || 800, height: svgRect.height || 400
1964
+ };
1965
+ } catch (err) {
1966
+ return { tag: 'MERMAID_FALLBACK' };
1967
+ }
1968
+ }
1969
+ return null;
1970
+ }
1971
+
1972
+ var tag = element.tagName;
1973
+ switch (tag) {
1974
+ case 'H1': return { tag: 'H1', text: element.textContent.trim() };
1975
+ case 'H2': return { tag: 'H2', text: element.textContent.trim(), styles: getElementStyles(element) };
1976
+ case 'H3': return { tag: 'H3', text: element.textContent.trim(), styles: getElementStyles(element) };
1977
+ case 'H4': return { tag: 'H4', text: element.textContent.trim(), styles: getElementStyles(element) };
1978
+ case 'P':
1979
+ var pText = element.textContent.trim();
1980
+ return pText ? { tag: 'P', text: pText } : null;
1981
+ case 'TABLE':
1982
+ return serializeTable(element);
1983
+ case 'UL':
1984
+ case 'OL':
1985
+ return { tag: tag, lines: processListToExcelText(element, '') };
1986
+ case 'PRE':
1987
+ var codeEl = element.querySelector('code');
1988
+ var codeText = codeEl ? codeEl.textContent : element.textContent;
1989
+ return codeText ? { tag: 'PRE', text: codeText } : null;
1990
+ case 'BLOCKQUOTE':
1991
+ var bqText = element.textContent.trim();
1992
+ return bqText ? { tag: 'BLOCKQUOTE', text: bqText } : null;
1993
+ case 'HR': return { tag: 'HR' };
1994
+ default: return null;
1995
+ }
1996
+ }
1997
+
1998
+ function serializeTable(table) {
1999
+ var rows = table.querySelectorAll('tr');
2000
+ var tableData = { tag: 'TABLE', rows: [] };
2001
+ rows.forEach(function(tr) {
2002
+ var cells = tr.querySelectorAll('th, td');
2003
+ var rowData = [];
2004
+ cells.forEach(function(cell) {
2005
+ rowData.push({
2006
+ text: cell.textContent.trim(),
2007
+ isHeader: cell.tagName === 'TH',
2008
+ colspan: parseInt(cell.getAttribute('colspan')) || 1
2009
+ });
2010
+ });
2011
+ tableData.rows.push(rowData);
2012
+ });
2013
+ return tableData;
2014
+ }
2034
2015
 
2035
2016
  if (exportExcelBtn) exportExcelBtn.addEventListener('click', async function() {
2036
2017
  try {
2037
2018
  await convertToExcel();
2038
2019
  } catch (error) {
2039
2020
  console.error('Excel変換エラー:', error);
2040
- alert('Excel変換中にエラーが発生しました: ' + error.message);
2021
+ var errMsg = error ? (error.message || error.toString()) : 'unknown';
2022
+ alert('Excel変換中にエラーが発生しました: ' + errMsg);
2041
2023
  }
2042
2024
  });
2043
2025
 
2044
2026
  async function convertToExcel() {
2045
- // ライブラリ遅延読み込み
2046
- await ensureExcelLibrary();
2047
-
2048
2027
  var content = container.querySelector('.content');
2049
2028
  if (!content) content = container;
2050
2029
 
@@ -2055,6 +2034,33 @@ document.addEventListener('DOMContentLoaded', function() {
2055
2034
  return;
2056
2035
  }
2057
2036
 
2037
+ // ファイル名取得
2038
+ var h1Element = container.querySelector('h1');
2039
+ var fileName = 'design_spec';
2040
+ if (h1Element) {
2041
+ fileName = h1Element.innerHTML
2042
+ .replace(/<br\s*\/?>/gi, '-')
2043
+ .replace(/<\/p>\s*<p>/gi, '-')
2044
+ .replace(/<[^>]*>/g, '')
2045
+ .replace(/\s+/g, '')
2046
+ .trim();
2047
+ }
2048
+
2049
+ // Chrome拡張のsandboxモード(window.__excelSandboxが設定されている場合)
2050
+ if (window.__excelSandbox) {
2051
+ var data = await serializeContentForExcel(sections, fileName);
2052
+ await window.__excelSandbox(data);
2053
+ return;
2054
+ }
2055
+
2056
+ // 通常モード(Web版 - CDNからExcelJS読み込み)
2057
+ await ensureExcelLibrary();
2058
+
2059
+ if (!window.ExcelJS) {
2060
+ alert('ExcelJSライブラリが読み込めませんでした');
2061
+ return;
2062
+ }
2063
+
2058
2064
  var workbook = new ExcelJS.Workbook();
2059
2065
  workbook.creator = 'Design Editor';
2060
2066
  workbook.created = new Date();
@@ -2110,7 +2116,7 @@ document.addEventListener('DOMContentLoaded', function() {
2110
2116
  if (h2Elements.length === 0) {
2111
2117
  var h1 = content.querySelector('h1');
2112
2118
  var title = h1 ? h1.textContent.trim() : 'Sheet1';
2113
- var elements = content.querySelectorAll('h1, h2, h3, h4, p, table, hr, ul, ol, pre, blockquote');
2119
+ var elements = content.querySelectorAll('h1, h2, h3, h4, p, table, hr, ul, ol, pre, blockquote, .mermaid-container');
2114
2120
  sections.push({
2115
2121
  title: title,
2116
2122
  elements: Array.from(elements),
@@ -2126,7 +2132,8 @@ document.addEventListener('DOMContentLoaded', function() {
2126
2132
  var next = h2.nextElementSibling;
2127
2133
 
2128
2134
  while (next && next.tagName !== 'H2' && next.tagName !== 'HR') {
2129
- if (['H3', 'H4', 'P', 'TABLE', 'UL', 'OL', 'PRE', 'BLOCKQUOTE'].includes(next.tagName)) {
2135
+ if (['H3', 'H4', 'P', 'TABLE', 'UL', 'OL', 'PRE', 'BLOCKQUOTE'].includes(next.tagName) ||
2136
+ (next.classList && next.classList.contains('mermaid-container'))) {
2130
2137
  elements.push(next);
2131
2138
  }
2132
2139
  next = next.nextElementSibling;
@@ -2273,6 +2280,45 @@ document.addEventListener('DOMContentLoaded', function() {
2273
2280
  }
2274
2281
 
2275
2282
  async function processElement(worksheet, element, currentRow, maxColumns) {
2283
+ // Mermaidコンテナの場合はDIVなのでclassで判定
2284
+ if (element.classList && element.classList.contains('mermaid-container')) {
2285
+ var svg = element.querySelector('svg');
2286
+ if (svg) {
2287
+ try {
2288
+ var pngBase64 = await svgToPngBase64(svg);
2289
+ var imageId = worksheet.workbook.addImage({
2290
+ base64: pngBase64,
2291
+ extension: 'png'
2292
+ });
2293
+
2294
+ // SVGの実サイズからExcel上のサイズを計算(縦横比保持)
2295
+ var svgRect = svg.getBoundingClientRect();
2296
+ var svgW = svgRect.width || 800;
2297
+ var svgH = svgRect.height || 400;
2298
+
2299
+ // Excel上の表示幅を600pxに制限し、縦横比を保持
2300
+ var maxWidthPx = 600;
2301
+ var displayW = Math.min(svgW, maxWidthPx);
2302
+ var displayH = svgH * (displayW / svgW);
2303
+ var rowsNeeded = Math.ceil(displayH / 20);
2304
+
2305
+ worksheet.addImage(imageId, {
2306
+ tl: { col: 0, row: currentRow - 1 },
2307
+ ext: { width: displayW, height: displayH }
2308
+ });
2309
+
2310
+ currentRow += rowsNeeded + 1;
2311
+ } catch (err) {
2312
+ // 画像変換失敗時はテキストで代替
2313
+ var fallbackCell = worksheet.getCell(currentRow, 1);
2314
+ fallbackCell.value = '[Mermaid図]';
2315
+ fallbackCell.font = { name: 'メイリオ', size: fontSizes.body, italic: true, color: { argb: 'FF999999' } };
2316
+ currentRow++;
2317
+ }
2318
+ }
2319
+ return currentRow;
2320
+ }
2321
+
2276
2322
  var tag = element.tagName;
2277
2323
 
2278
2324
  switch (tag) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunny-html-editor",
3
- "version": "1.2.5",
3
+ "version": "1.3.1",
4
4
  "description": "軽量WYSIWYGエディタ - 既存のHTMLに編集機能を後付けで追加、Markdown変換対応",
5
5
  "main": "html-editor.js",
6
6
  "files": [
@@ -13,7 +13,8 @@
13
13
  "editor",
14
14
  "html",
15
15
  "contenteditable",
16
- "markdown"
16
+ "markdown",
17
+ "mermaid"
17
18
  ],
18
19
  "author": "",
19
20
  "license": "MIT"