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.
- package/README.md +65 -104
- package/html-editor.js +497 -31
- 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
|
-
|
|
17
|
+
### CDN(推奨)
|
|
28
18
|
|
|
29
19
|
```html
|
|
30
|
-
|
|
31
|
-
<script src="https://
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
```
|
|
86
|
-
|
|
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
|
-
|
|
109
|
+
| ボタン | 機能 |
|
|
110
|
+
|--------|------|
|
|
111
|
+
| ↔️ | サイドバー表示/非表示 |
|
|
112
|
+
| 📝 | 編集モード切替 |
|
|
113
|
+
| Ⓜ️ | Markdownコピー |
|
|
114
|
+
| 🌐 | HTMLコピー |
|
|
115
|
+
| 💾 | HTML保存(ダウンロード) |
|
|
116
|
+
| 📊 | Excel出力 |
|
|
117
|
+
| 📕 | PDF出力 |
|
|
140
118
|
|
|
141
|
-
|
|
142
|
-
読み込まれていない場合、ボタンを押すとアラートが表示されます。
|
|
119
|
+
## バージョン履歴
|
|
143
120
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
* @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, '&')
|
|
437
|
+
.replace(/</g, '<')
|
|
438
|
+
.replace(/>/g, '>')
|
|
439
|
+
.replace(/"/g, '"')
|
|
440
|
+
.replace(/'/g, ''');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ========== 初期化 ==========
|
|
5
444
|
document.addEventListener('DOMContentLoaded', function() {
|
|
6
445
|
var container = document.getElementById('editorContainer');
|
|
7
|
-
if (!container) return;
|
|
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
|
-
|
|
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
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'<
|
|
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
|
-
'
|
|
163
|
-
'
|
|
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