sunny-html-editor 1.0.3 → 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.
Files changed (3) hide show
  1. package/README.md +65 -104
  2. package/html-editor.js +497 -31
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,94 +1,85 @@
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テキストを自動変換
9
+ - **遅延読み込み** - Excel/PDF出力用ライブラリは使用時に自動読み込み
8
10
  - **WYSIWYG編集** - 見たまま編集、右クリックで要素追加
9
11
  - **キーボード操作** - ↑↓で要素間移動、Shift/Ctrl+Enterで項目追加
10
12
  - **多彩なエクスポート** - Markdown、HTML、Excel、PDF
11
13
  - **サイドバーナビ** - 目次自動生成
12
14
 
13
- ## 外部ライブラリ依存
14
-
15
- ### 必須
16
-
17
- なし(基本機能のみ使用する場合)
18
-
19
- ### オプション(Excel/PDF出力に必要)
20
-
21
- | ライブラリ | バージョン | 用途 | CDN |
22
- |-----------|----------|------|-----|
23
- | ExcelJS | 4.3.0+ | Excel出力 | https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js |
24
- | jsPDF | 2.5.1+ | PDF出力 | https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js |
25
- | html2canvas | 1.4.1+ | PDF出力(HTML→Canvas変換) | https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js |
15
+ ## インストール
26
16
 
27
- Excel/PDF出力を使う場合は、html-editor.jsより**前に**読み込んでください:
17
+ ### CDN(推奨)
28
18
 
29
19
  ```html
30
- <!-- 外部ライブラリ(Excel/PDF出力に必要) -->
31
- <script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js"></script>
32
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
33
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
34
-
35
- <!-- HTML Editor -->
36
- <link rel="stylesheet" href="html-editor.css">
37
- <script src="html-editor.js"></script>
20
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sunny-html-editor/html-editor.css">
21
+ <script src="https://cdn.jsdelivr.net/npm/sunny-html-editor/html-editor.js"></script>
38
22
  ```
39
23
 
40
- ## インストール
41
-
42
24
  ### npm
43
25
 
44
26
  ```bash
45
27
  npm install sunny-html-editor
46
28
  ```
47
29
 
48
- ### CDN
49
-
50
- ```html
51
- <link rel="stylesheet" href="https://unpkg.com/sunny-html-editor/html-editor.css">
52
- <script src="https://unpkg.com/sunny-html-editor/html-editor.js"></script>
53
- ```
54
-
55
- ### ダウンロード
56
-
57
- `html-editor.css` と `html-editor.js` をダウンロードして読み込み。
58
-
59
30
  ## 使い方
60
31
 
61
- ### 基本
32
+ ### 基本(HTMLコンテンツ)
33
+
34
+ `id="editorContainer"` を持つコンテナを用意するだけ。ツールバーとコンテキストメニューは自動生成されます。
62
35
 
63
36
  ```html
64
37
  <!DOCTYPE html>
65
- <html>
38
+ <html lang="ja">
66
39
  <head>
67
- <link rel="stylesheet" href="html-editor.css">
40
+ <meta charset="UTF-8">
41
+ <title>ドキュメントタイトル</title>
42
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sunny-html-editor/html-editor.css">
68
43
  </head>
69
44
  <body>
70
- <div id="content">
71
- <h1>タイトル</h1>
72
- <p>既存のHTMLコンテンツ</p>
73
- </div>
74
-
75
- <script src="html-editor.js"></script>
76
- <script>
77
- HtmlEditor.init('#content');
78
- </script>
45
+ <div class="layout">
46
+ <main class="main">
47
+ <div class="container" id="editorContainer">
48
+ <h1>タイトル</h1>
49
+ <h2 id="section1">見出し</h2>
50
+ <p>本文...</p>
51
+ </div>
52
+ </main>
53
+ </div>
54
+ <script src="https://cdn.jsdelivr.net/npm/sunny-html-editor/html-editor.js"></script>
79
55
  </body>
80
56
  </html>
81
57
  ```
82
58
 
83
- ### オプション
59
+ ### Markdown変換モード
60
+
61
+ `data-markdown`属性にURLエンコードしたMarkdownテキストを設定すると、自動でHTMLに変換されます。
84
62
 
85
- ```javascript
86
- HtmlEditor.init('#content', {
87
- sidebar: true, // サイドバー表示(デフォルト: true)
88
- toolbar: true // ツールバー表示(デフォルト: true)
89
- });
63
+ ```html
64
+ <div class="container" id="editorContainer" data-markdown="URLエンコードされたMDテキスト"></div>
90
65
  ```
91
66
 
67
+ JavaScript側で自動的に:
68
+ 1. `data-markdown`属性をデコード
69
+ 2. Markdown → HTML変換
70
+ 3. コンテナ内にHTMLを生成
71
+ 4. `data-markdown`属性を削除(HTML保存時に残らないように)
72
+
73
+ ## 外部ライブラリ
74
+
75
+ Excel/PDF出力に必要なライブラリは**使用時に自動で読み込まれます**。事前の読み込みは不要です。
76
+
77
+ | ライブラリ | 用途 |
78
+ |-----------|------|
79
+ | ExcelJS | Excel出力 |
80
+ | jsPDF | PDF出力 |
81
+ | html2canvas | PDF出力(HTML→Canvas変換) |
82
+
92
83
  ## キーボードショートカット
93
84
 
94
85
  | キー | 動作 |
@@ -113,64 +104,34 @@ HtmlEditor.init('#content', {
113
104
 
114
105
  テーブル上では行/列の追加・削除も可能。
115
106
 
116
- ## エクスポート
117
-
118
- ### Markdown
119
-
120
- ```javascript
121
- // ツールバーのⓂ️ボタン、または Ctrl+M
122
- // クリップボードにコピーされます
123
- ```
124
-
125
- ### HTML
126
-
127
- ```javascript
128
- // ツールバーの🌐ボタン
129
- // クリップボードにコピーされます
130
- ```
131
-
132
- ### HTML保存
133
-
134
- ```javascript
135
- // ツールバーの💾ボタン
136
- // HTMLファイルとしてダウンロード
137
- ```
107
+ ## ツールバーボタン
138
108
 
139
- ### Excel / PDF
109
+ | ボタン | 機能 |
110
+ |--------|------|
111
+ | ↔️ | サイドバー表示/非表示 |
112
+ | 📝 | 編集モード切替 |
113
+ | Ⓜ️ | Markdownコピー |
114
+ | 🌐 | HTMLコピー |
115
+ | 💾 | HTML保存(ダウンロード) |
116
+ | 📊 | Excel出力 |
117
+ | 📕 | PDF出力 |
140
118
 
141
- 上記の外部ライブラリが読み込まれている必要があります。
142
- 読み込まれていない場合、ボタンを押すとアラートが表示されます。
119
+ ## バージョン履歴
143
120
 
144
- ## API
145
-
146
- ### HtmlEditor.init(selector, options)
147
-
148
- エディタを初期化します。
149
-
150
- - `selector` - コンテナ要素のセレクタまたはDOM要素
151
- - `options` - オプションオブジェクト
152
-
153
- ### 返り値
154
-
155
- HtmlEditorインスタンスを返します。
156
-
157
- ```javascript
158
- var editor = HtmlEditor.init('#content');
159
-
160
- // 編集モード切替
161
- editor._action_editMode();
162
-
163
- // サイドバー更新
164
- editor._updateSidebar();
165
- ```
121
+ - **1.2.0** - Markdown変換機能追加(`data-markdown`属性対応)
122
+ - **1.1.0** - ツールバー・コンテキストメニュー自動生成、外部ライブラリ遅延読み込み
123
+ - **1.0.3** - nullチェック強化
124
+ - **1.0.2** - DOMContentLoaded自動初期化
125
+ - **1.0.1** - メディアクエリ削除
126
+ - **1.0.0** - 初回リリース
166
127
 
167
128
  ## ブラウザ対応
168
129
 
169
- - Chrome (推奨)
130
+ - Chrome(推奨)
170
131
  - Firefox
171
132
  - Safari
172
133
  - Edge
173
134
 
174
135
  ## ライセンス
175
136
 
176
- MIT
137
+ MIT License - OnMax Group
package/html-editor.js CHANGED
@@ -1,11 +1,471 @@
1
1
  /**
2
2
  * Sunny HTML Editor - 軽量WYSIWYGエディタ
3
- * @version 1.0.3
3
+ * @version 1.1.0
4
4
  */
5
+
6
+ // ========== HTML生成関数 ==========
7
+ function getToolbarHtml() {
8
+ return '<div class="toolbar">' +
9
+ '<button id="toggleSidebar" title="サイドメニュー表示/非表示 (Ctrl+B)">↔️</button>' +
10
+ '<button id="editMode" title="編集モード (Ctrl+E)">📝</button>' +
11
+ '<button id="exportMd" title="Markdownコピー (Ctrl+M)">Ⓜ️</button>' +
12
+ '<button id="exportHtml" title="HTMLコピー">🌐</button>' +
13
+ '<button id="saveHtml" title="HTML保存">💾</button>' +
14
+ '<button id="exportExcel" title="Excel出力">📊</button>' +
15
+ '<button id="exportPdf" title="PDF出力">📕</button>' +
16
+ '</div>';
17
+ }
18
+
19
+ function getContextMenusHtml() {
20
+ // テーブル用
21
+ var tableMenu = '<div class="context-menu" id="contextMenu">' +
22
+ '<div class="context-menu-item" data-action="addRowBelow">+ 行を追加</div>' +
23
+ '<div class="context-menu-item" data-action="addColRight">+ 列を追加</div>' +
24
+ '<div class="context-menu-divider"></div>' +
25
+ '<div class="context-menu-item delete" data-action="deleteRow">🗑 行を削除</div>' +
26
+ '<div class="context-menu-item delete" data-action="deleteCol">🗑 列を削除</div>' +
27
+ '<div class="context-menu-item delete" data-action="deleteTable">🗑 テーブルを削除</div>' +
28
+ '<div class="context-menu-divider thick"></div>' +
29
+ '<div class="context-menu-item" data-action="addH2">+ 大見出し</div>' +
30
+ '<div class="context-menu-item" data-action="addH3">+ 中見出し</div>' +
31
+ '<div class="context-menu-item" data-action="addH4">+ 小見出し</div>' +
32
+ '<div class="context-menu-divider"></div>' +
33
+ '<div class="context-menu-item" data-action="addTableBelow">+ テーブル</div>' +
34
+ '<div class="context-menu-divider"></div>' +
35
+ '<div class="context-menu-item" data-action="addParagraph">+ 本文</div>' +
36
+ '<div class="context-menu-divider"></div>' +
37
+ '<div class="context-menu-item" data-action="addUl">+ 箇条書き</div>' +
38
+ '<div class="context-menu-item" data-action="addOl">+ 番号リスト</div>' +
39
+ '<div class="context-menu-divider"></div>' +
40
+ '<div class="context-menu-item" data-action="addPre">+ コードブロック</div>' +
41
+ '<div class="context-menu-item" data-action="addBlockquote">+ 引用</div>' +
42
+ '</div>';
43
+
44
+ // 共通メニュー項目
45
+ var commonItems =
46
+ '<div class="context-menu-item" data-action="addH2">+ 大見出し</div>' +
47
+ '<div class="context-menu-item" data-action="addH3">+ 中見出し</div>' +
48
+ '<div class="context-menu-item" data-action="addH4">+ 小見出し</div>' +
49
+ '<div class="context-menu-divider"></div>' +
50
+ '<div class="context-menu-item" data-action="addTable">+ テーブル</div>' +
51
+ '<div class="context-menu-divider"></div>' +
52
+ '<div class="context-menu-item" data-action="addParagraph">+ 本文</div>' +
53
+ '<div class="context-menu-divider"></div>' +
54
+ '<div class="context-menu-item" data-action="addUl">+ 箇条書き</div>' +
55
+ '<div class="context-menu-item" data-action="addOl">+ 番号リスト</div>' +
56
+ '<div class="context-menu-divider"></div>' +
57
+ '<div class="context-menu-item" data-action="addPre">+ コードブロック</div>' +
58
+ '<div class="context-menu-item" data-action="addBlockquote">+ 引用</div>';
59
+
60
+ // h1用
61
+ var h1Menu = '<div class="context-menu" id="h1ContextMenu">' + commonItems + '</div>';
62
+
63
+ // 見出し用
64
+ var headingMenu = '<div class="context-menu" id="headingContextMenu">' + commonItems +
65
+ '<div class="context-menu-divider"></div>' +
66
+ '<div class="context-menu-item delete" data-action="deleteSection">🗑 セクションを削除</div>' +
67
+ '</div>';
68
+
69
+ // 段落用
70
+ var paragraphMenu = '<div class="context-menu" id="paragraphContextMenu">' + commonItems +
71
+ '<div class="context-menu-divider"></div>' +
72
+ '<div class="context-menu-item delete" data-action="deleteParagraph">🗑 本文を削除</div>' +
73
+ '</div>';
74
+
75
+ // リスト用
76
+ var listMenu = '<div class="context-menu" id="listContextMenu">' + commonItems +
77
+ '<div class="context-menu-divider"></div>' +
78
+ '<div class="context-menu-item delete" data-action="deleteList">🗑 リストを削除</div>' +
79
+ '</div>';
80
+
81
+ // コードブロック用
82
+ var preMenu = '<div class="context-menu" id="preContextMenu">' + commonItems +
83
+ '<div class="context-menu-divider"></div>' +
84
+ '<div class="context-menu-item delete" data-action="deletePre">🗑 コードブロックを削除</div>' +
85
+ '</div>';
86
+
87
+ // 引用用
88
+ var blockquoteMenu = '<div class="context-menu" id="blockquoteContextMenu">' + commonItems +
89
+ '<div class="context-menu-divider"></div>' +
90
+ '<div class="context-menu-item delete" data-action="deleteBlockquote">🗑 引用を削除</div>' +
91
+ '</div>';
92
+
93
+ return tableMenu + h1Menu + headingMenu + paragraphMenu + listMenu + preMenu + blockquoteMenu;
94
+ }
95
+
96
+ // ========== 外部ライブラリ遅延読み込み ==========
97
+ var libraryPromises = {};
98
+
99
+ function loadScript(src) {
100
+ if (libraryPromises[src]) return libraryPromises[src];
101
+
102
+ libraryPromises[src] = new Promise(function(resolve, reject) {
103
+ var script = document.createElement('script');
104
+ script.src = src;
105
+ script.onload = resolve;
106
+ script.onerror = reject;
107
+ document.head.appendChild(script);
108
+ });
109
+
110
+ return libraryPromises[src];
111
+ }
112
+
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');
116
+ }
117
+ if (!window.html2canvas) {
118
+ await loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js');
119
+ }
120
+ }
121
+
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
+ }
126
+ }
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
+
443
+ // ========== 初期化 ==========
5
444
  document.addEventListener('DOMContentLoaded', function() {
6
445
  var container = document.getElementById('editorContainer');
7
- if (!container) return; // editorContainerがなければ何もしない
446
+ if (!container) return;
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
+
462
+ // ツールバー挿入
463
+ container.insertAdjacentHTML('afterbegin', getToolbarHtml());
8
464
 
465
+ // コンテキストメニュー挿入(bodyの末尾に配置)
466
+ document.body.insertAdjacentHTML('beforeend', getContextMenusHtml());
467
+
468
+ // ボタン参照取得
9
469
  var editModeBtn = document.getElementById('editMode');
10
470
  var toggleSidebarBtn = document.getElementById('toggleSidebar');
11
471
  var saveHtmlBtn = document.getElementById('saveHtml');
@@ -47,7 +507,25 @@ document.addEventListener('DOMContentLoaded', function() {
47
507
  var wasEditMode = isEditMode;
48
508
  if (isEditMode && editModeBtn) editModeBtn.click();
49
509
 
50
- var html = '<!DOCTYPE html>\n' + document.documentElement.outerHTML;
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');
521
+ if (cloneToolbar) cloneToolbar.remove();
522
+ cloneMenus.forEach(function(menu) { menu.remove(); });
523
+
524
+ var html = '<!DOCTYPE html>\n' + docClone.outerHTML;
525
+
526
+ // 表示を復元
527
+ if (toolbar) toolbar.style.display = '';
528
+ contextMenus.forEach(function(menu) { menu.style.display = ''; });
51
529
 
52
530
  if (wasEditMode && editModeBtn) editModeBtn.click();
53
531
 
@@ -104,7 +582,7 @@ document.addEventListener('DOMContentLoaded', function() {
104
582
  var toolbar = content.querySelector('.toolbar');
105
583
  if (toolbar) toolbar.remove();
106
584
 
107
- // 右クリックメニューを除去
585
+ // 右クリックメニューを除去(念のため)
108
586
  content.querySelectorAll('.context-menu').forEach(function(menu) {
109
587
  menu.remove();
110
588
  });
@@ -130,41 +608,23 @@ document.addEventListener('DOMContentLoaded', function() {
130
608
  var h1 = container.querySelector('h1');
131
609
  var title = h1 ? h1.textContent.trim() : 'Document';
132
610
 
133
- // CSSを取得(サイドバー、リサイザー、ツールバー、右クリックメニュー関連を除外)
134
- var styleEl = document.querySelector('style');
135
- var css = '';
136
- if (styleEl) {
137
- css = styleEl.textContent
138
- .replace(/\.sidebar[\s\S]*?\}\n/g, '')
139
- .replace(/\.sidebar [^{]*\{[\s\S]*?\}\n/g, '')
140
- .replace(/\.resizer[\s\S]*?\}\n/g, '')
141
- .replace(/\.toolbar[\s\S]*?\}\n/g, '')
142
- .replace(/\.toolbar [^{]*\{[\s\S]*?\}\n/g, '')
143
- .replace(/\.context-menu[\s\S]*?\}\n/g, '')
144
- .replace(/\.context-menu[^{]*\{[\s\S]*?\}\n/g, '')
145
- .replace(/\.edit-mode[\s\S]*?\}\n/g, '')
146
- .replace(/\.edit-mode [^{]*\{[\s\S]*?\}\n/g, '')
147
- .replace(/@media[^{]*\{[\s\S]*?\}\s*\}/g, '')
148
- .replace(/\n\s*\n/g, '\n')
149
- .trim();
150
- }
151
-
152
- // 完全なHTML構造(CSS含む、ラッパー構造含む)
611
+ // 完全なHTML構造(外部CSS/JS参照付き)
153
612
  var html = '<!DOCTYPE html>\n' +
154
613
  '<html lang="ja">\n' +
155
614
  '<head>\n' +
156
- ' <meta charset="UTF-8">\n' +
157
- ' <title>' + title + '</title>\n' +
158
- '<style>\n' + css + '\n</style>\n' +
615
+ '<meta charset="UTF-8">\n' +
616
+ '<title>' + title + '</title>\n' +
617
+ '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sunny-html-editor/html-editor.css">\n' +
159
618
  '</head>\n' +
160
619
  '<body>\n' +
161
620
  '<div class="layout">\n' +
162
- ' <main class="main">\n' +
163
- ' <div class="container">\n' +
621
+ '<main class="main">\n' +
622
+ '<div class="container" id="editorContainer">\n' +
164
623
  innerHtml + '\n' +
165
- ' </div>\n' +
166
- ' </main>\n' +
167
624
  '</div>\n' +
625
+ '</main>\n' +
626
+ '</div>\n' +
627
+ '<script src="https://cdn.jsdelivr.net/npm/sunny-html-editor/html-editor.js"><\/script>\n' +
168
628
  '</body>\n' +
169
629
  '</html>';
170
630
 
@@ -1371,6 +1831,9 @@ document.addEventListener('DOMContentLoaded', function() {
1371
1831
  exportPdfBtn.textContent = '...';
1372
1832
 
1373
1833
  try {
1834
+ // ライブラリ遅延読み込み
1835
+ await ensurePdfLibraries();
1836
+
1374
1837
  // jsPDFインスタンス作成
1375
1838
  var { jsPDF } = window.jspdf;
1376
1839
  var pdf = new jsPDF({
@@ -1573,6 +2036,9 @@ document.addEventListener('DOMContentLoaded', function() {
1573
2036
  });
1574
2037
 
1575
2038
  async function convertToExcel() {
2039
+ // ライブラリ遅延読み込み
2040
+ await ensureExcelLibrary();
2041
+
1576
2042
  var content = container.querySelector('.content');
1577
2043
  if (!content) content = container;
1578
2044
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sunny-html-editor",
3
- "version": "1.0.3",
4
- "description": "軽量WYSIWYGエディタ - 既存のHTMLに編集機能を後付けで追加",
3
+ "version": "1.2.0",
4
+ "description": "軽量WYSIWYGエディタ - 既存のHTMLに編集機能を後付けで追加、Markdown変換対応",
5
5
  "main": "html-editor.js",
6
6
  "files": [
7
7
  "html-editor.js",