ponkotsu-md-editor 0.2.38 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5302c9a2279a1cc63c0eb45bc2bef74a3293fc792c4c1b21b73d69368fe75054
4
- data.tar.gz: 496540544d96bf811322e4de5338fc6cab9d6706c0d061d545272e64630ae528
3
+ metadata.gz: 69a042675752a90ebfb4fea9302b8afdcdc4ef5b5a0a92d1060bb5c591c591c9
4
+ data.tar.gz: 1ba8795a6ce803ddc14c64df8d709e46829bbcfc36915161d4577fb947ad2e33
5
5
  SHA512:
6
- metadata.gz: 040fc09a76dec0d164fd9190b297cfc63acd4a8059d74a3ee8a492bf2888f11dbaba03b194b83ac285609d522b3cd359b62e273a257bf3a4e829af7e9d8ab592
7
- data.tar.gz: 8f0cfef74e8bc86cdffb916f5bf7af964578dae487288d3b46da2b91dff1331767d9a045a1157baad4476b64c9981a40258b7b2c15eb53561da4db95d33f3152
6
+ metadata.gz: e21f8d1f60a1cbedec438c055b9522a27714dd1bb874130b89d182fe9b62ea15fc5930f576b46b2df84ac79628b662ba14fcf0587ef571cf9766eda9caa068b0
7
+ data.tar.gz: 021bb8b7babf193b14a9baada9ced767fcacdc8b09d1990c0453dcc384b62aaea8f40366db51037e8ce09e216fadd637473cc0372e9161975f90ac551a3365b2
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.7
data/E2E_TESTING.md ADDED
@@ -0,0 +1,256 @@
1
+ # E2E Testing Guide for Ponkotsu Markdown Editor
2
+
3
+ このドキュメントでは、ponkotsu-md-editorのEnd-to-End(E2E)テストの実行方法について説明します。
4
+
5
+ ## 概要
6
+
7
+ E2Eテストは、実際のブラウザ環境でMarkdownエディタの機能を検証します。以下のブラウザでテストを実行できます:
8
+
9
+ - Google Chrome (Headless)
10
+ - Mozilla Firefox (Headless)
11
+
12
+ ## 前提条件
13
+
14
+ ### 必要なソフトウェア
15
+
16
+ 1. **Ruby** (>= 3.4.7)
17
+ 2. **Google Chrome** (最新版)
18
+ 3. **Mozilla Firefox** (最新版)
19
+ 4. **ChromeDriver** (webdrivers gemが自動的に管理)
20
+ 5. **GeckoDriver** (webdrivers gemが自動的に管理)
21
+
22
+ ### Gemのインストール
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ 必要なgemは以下の通りです:
29
+ - `rspec`: テストフレームワーク
30
+ - `capybara`: ブラウザ自動化ツール
31
+ - `selenium-webdriver`: WebDriverプロトコルの実装
32
+ - `webdrivers`: ブラウザドライバーの自動管理
33
+
34
+ ## テストファイルの構成
35
+
36
+ ```
37
+ spec/
38
+ ├── spec_helper.rb # RSpecとCapybaraの設定
39
+ ├── support/
40
+ │ └── e2e_helpers.rb # E2Eテスト用ヘルパーメソッド
41
+ └── features/
42
+ ├── markdown_editor_e2e_spec.rb # 包括的なE2Eテスト
43
+ └── bold_functionality_spec.rb # Bold機能の詳細テスト
44
+ ```
45
+
46
+ ## テストの実行方法
47
+
48
+ ### すべてのテストを実行
49
+
50
+ ```bash
51
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb
52
+ ```
53
+
54
+ ### 特定のブラウザでのみ実行
55
+
56
+ **Chromeのみ:**
57
+ ```bash
58
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb -e "Testing with Chrome"
59
+ ```
60
+
61
+ **Firefoxのみ:**
62
+ ```bash
63
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb -e "Testing with Firefox"
64
+ ```
65
+
66
+ ### 特定のテストカテゴリを実行
67
+
68
+ **基本操作のみ:**
69
+ ```bash
70
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb -e "Basic operations"
71
+ ```
72
+
73
+ **装飾ボタンのみ:**
74
+ ```bash
75
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb -e "Formatting buttons"
76
+ ```
77
+
78
+ **プレビュー機能のみ:**
79
+ ```bash
80
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb -e "Preview functionality"
81
+ ```
82
+
83
+ ### ヘッドレスモードを無効にしてテストを実行
84
+
85
+ ブラウザの動作を視覚的に確認したい場合は、spec_helper.rbを編集してヘッドレスオプションを削除します:
86
+
87
+ ```ruby
88
+ # spec/spec_helper.rbの該当箇所をコメントアウト
89
+ # options.add_argument("--headless")
90
+ ```
91
+
92
+ ## テスト項目
93
+
94
+ ### 1. 基本操作テスト
95
+
96
+ - **テキスト挿入**
97
+ - 英数字のテキスト挿入
98
+ - 日本語テキストの挿入
99
+ - 改行の保持
100
+ - 複数の連続した改行の挿入
101
+
102
+ - **テキスト削除**
103
+ - 選択範囲の削除
104
+ - コンテンツ全体のクリア
105
+
106
+ - **カーソル位置**
107
+ - テキスト挿入後のカーソル位置維持
108
+
109
+ ### 2. 装飾ボタン機能テスト
110
+
111
+ 各Markdown装飾機能が正しく動作することを確認:
112
+
113
+ - **Bold** (`**text**`)
114
+ - **Italic** (`*text*`)
115
+ - **Strikethrough** (`~~text~~`)
116
+ - **Code** (`` `code` `` or ` ```code block``` `)
117
+ - **Headings** (`# H1`, `## H2`, `### H3`, etc.)
118
+ - **Lists**
119
+ - Unordered list (`- item`)
120
+ - Ordered list (`1. item`)
121
+ - Checkbox list (`- [ ] task`, `- [x] done`)
122
+ - **Blockquote** (`> quote`)
123
+ - **Link** (`[text](url)`)
124
+ - **Image** (`![alt](url)`)
125
+ - **Table** (マークダウンテーブル構文)
126
+ - **Horizontal Rule** (`---`)
127
+
128
+ ### 3. 複数の装飾操作テスト
129
+
130
+ - 異なるテキスト範囲への複数の装飾適用
131
+ - ネストされた装飾の処理
132
+
133
+ ### 4. プレビュー機能テスト
134
+
135
+ - プレビューの表示/非表示切り替え
136
+ - MarkdownからHTMLへの変換
137
+
138
+ ### 5. エッジケースとエラーハンドリング
139
+
140
+ - 空のエディタでの操作
141
+ - 非常に長いテキストの処理
142
+ - 特殊文字の処理
143
+ - Unicode文字(絵文字、多言語)の処理
144
+
145
+ ## テストのデバッグ
146
+
147
+ ### ログの有効化
148
+
149
+ ```ruby
150
+ # spec/spec_helper.rbに追加
151
+ RSpec.configure do |config|
152
+ config.before(:each, type: :feature) do
153
+ page.driver.browser.manage.logs.get(:browser).each do |log|
154
+ puts log.message
155
+ end
156
+ end
157
+ end
158
+ ```
159
+
160
+ ### スクリーンショットの取得
161
+
162
+ テスト失敗時にスクリーンショットを保存:
163
+
164
+ ```ruby
165
+ # テストケース内で
166
+ save_screenshot('debug_screenshot.png')
167
+ ```
168
+
169
+ ### ブラウザコンソールログの確認
170
+
171
+ ```ruby
172
+ page.driver.browser.manage.logs.get(:browser).each do |log|
173
+ puts "[#{log.level}] #{log.message}"
174
+ end
175
+ ```
176
+
177
+ ## トラブルシューティング
178
+
179
+ ### ChromeDriverのバージョンエラー
180
+
181
+ ```bash
182
+ # webdriversキャッシュをクリア
183
+ rm -rf ~/.webdrivers
184
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb
185
+ ```
186
+
187
+ ### GeckoDriverのエラー
188
+
189
+ Firefoxが最新版であることを確認してください:
190
+
191
+ ```bash
192
+ # macOS
193
+ brew upgrade firefox
194
+ ```
195
+
196
+ ### タイムアウトエラー
197
+
198
+ Capybaraのタイムアウト設定を増やす:
199
+
200
+ ```ruby
201
+ # spec/spec_helper.rb
202
+ Capybara.configure do |config|
203
+ config.default_max_wait_time = 20 # デフォルトは10秒
204
+ end
205
+ ```
206
+
207
+ ## CI/CD環境での実行
208
+
209
+ ### GitHub Actions設定例
210
+
211
+ ```yaml
212
+ name: E2E Tests
213
+
214
+ on: [push, pull_request]
215
+
216
+ jobs:
217
+ e2e:
218
+ runs-on: ubuntu-latest
219
+
220
+ steps:
221
+ - uses: actions/checkout@v3
222
+
223
+ - name: Set up Ruby
224
+ uses: ruby/setup-ruby@v1
225
+ with:
226
+ ruby-version: 3.4.7
227
+ bundler-cache: true
228
+
229
+ - name: Install Chrome
230
+ run: |
231
+ wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
232
+ sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
233
+ sudo apt-get update
234
+ sudo apt-get install -y google-chrome-stable
235
+
236
+ - name: Install Firefox
237
+ run: sudo apt-get install -y firefox
238
+
239
+ - name: Run E2E tests
240
+ run: bundle exec rspec spec/features/markdown_editor_e2e_spec.rb
241
+ ```
242
+
243
+ ## ベストプラクティス
244
+
245
+ 1. **テストの独立性**: 各テストは独立して実行可能にする
246
+ 2. **適切な待機時間**: `sleep`の代わりにCapybaraの待機メソッドを使用
247
+ 3. **クリーンアップ**: 各テスト後にエディタの状態をリセット
248
+ 4. **エラーメッセージ**: 失敗時に有用な情報を提供
249
+ 5. **並列実行**: テストを並列実行して時間を短縮(`parallel_tests` gemを使用)
250
+
251
+ ## 参考リンク
252
+
253
+ - [Capybara Documentation](https://github.com/teamcapybara/capybara)
254
+ - [Selenium WebDriver Documentation](https://www.selenium.dev/documentation/webdriver/)
255
+ - [RSpec Documentation](https://rspec.info/)
256
+ - [WebDrivers Gem](https://github.com/titusfortner/webdrivers)
data/README.md CHANGED
@@ -105,9 +105,36 @@ params[:model][:content] # => Markdown text
105
105
 
106
106
  This gem mainly provides Rails view elements (helpers, partials, JS/CSS assets), so UI and behavior cannot be automatically tested with standard RSpec, etc.
107
107
 
108
+ ### E2Eテスト/E2E Testing
108
109
 
109
- - UIやエディタの動作確認には、手動テストを推奨します。
110
- - For UI/editor behavior, manual testing is recommended.
110
+ ChromeとFirefoxを使用したE2Eテストを用意しています。
111
+
112
+ E2E tests using Chrome and Firefox are available.
113
+
114
+ ```bash
115
+ # すべてのテストを実行/Run all tests
116
+ bundle exec rspec spec/features/markdown_editor_e2e_spec.rb
117
+
118
+ # 簡単にテストを実行/Run tests easily
119
+ bin/run_e2e_tests
120
+
121
+ # 特定のブラウザでテスト/Test with specific browser
122
+ bin/run_e2e_tests --browser chrome
123
+ bin/run_e2e_tests --browser firefox
124
+
125
+ # 特定のカテゴリをテスト/Test specific category
126
+ bin/run_e2e_tests --category basic
127
+ bin/run_e2e_tests --category formatting
128
+ ```
129
+
130
+ 詳細は [E2E_TESTING.md](E2E_TESTING.md) を参照してください。
131
+
132
+ See [E2E_TESTING.md](E2E_TESTING.md) for more details.
133
+
134
+ ### 手動テスト/Manual Testing
135
+
136
+ - UIやエディタの動作確認には、手動テストも推奨します。
137
+ - Manual testing is also recommended for UI/editor behavior verification.
111
138
 
112
139
  ## コントリビュート/Contributing
113
140
 
@@ -5,6 +5,59 @@
5
5
  let _scanOffsetCache = new Map();
6
6
 
7
7
  const analyzeHtmlCache = new Map();
8
+ const MAX_CACHE_SIZE = 500;
9
+
10
+ // Undo/Redo stack management
11
+ class UndoRedoManager {
12
+ constructor(maxStackSize = 100) {
13
+ this.undoStack = [];
14
+ this.redoStack = [];
15
+ this.maxStackSize = maxStackSize;
16
+ this.isUndoRedoAction = false;
17
+ }
18
+
19
+ pushState(state) {
20
+ if (this.isUndoRedoAction) return;
21
+
22
+ this.undoStack.push(state);
23
+ if (this.undoStack.length > this.maxStackSize) {
24
+ this.undoStack.shift();
25
+ }
26
+ // Clear redo stack when new state is pushed
27
+ this.redoStack = [];
28
+ }
29
+
30
+ undo() {
31
+ if (this.undoStack.length === 0) return null;
32
+
33
+ const currentState = this.undoStack.pop();
34
+ this.redoStack.push(currentState);
35
+
36
+ return this.undoStack.length > 0 ? this.undoStack[this.undoStack.length - 1] : null;
37
+ }
38
+
39
+ redo() {
40
+ if (this.redoStack.length === 0) return null;
41
+
42
+ const state = this.redoStack.pop();
43
+ this.undoStack.push(state);
44
+
45
+ return state;
46
+ }
47
+
48
+ canUndo() {
49
+ return this.undoStack.length > 1;
50
+ }
51
+
52
+ canRedo() {
53
+ return this.redoStack.length > 0;
54
+ }
55
+
56
+ clear() {
57
+ this.undoStack = [];
58
+ this.redoStack = [];
59
+ }
60
+ }
8
61
 
9
62
  // Debounce function for delayed execution
10
63
  function debounce(func, wait) {
@@ -142,6 +195,80 @@
142
195
  // 初期化時に同期
143
196
  syncToHidden();
144
197
 
198
+ // inputイベントで同期
199
+ textarea.addEventListener('input', syncToHidden);
200
+
201
+ // Undo/Redo manager initialization
202
+ const undoRedoManager = new UndoRedoManager();
203
+
204
+ // Save editor state
205
+ function saveEditorState() {
206
+ const state = {
207
+ html: textarea.innerHTML,
208
+ text: textarea.innerText,
209
+ timestamp: Date.now()
210
+ };
211
+ undoRedoManager.pushState(state);
212
+ }
213
+
214
+ // Restore editor state
215
+ function restoreEditorState(state) {
216
+ if (!state) return;
217
+
218
+ undoRedoManager.isUndoRedoAction = true;
219
+ textarea.innerHTML = state.html;
220
+ syncToHidden();
221
+ undoRedoManager.isUndoRedoAction = false;
222
+ }
223
+
224
+ // Perform undo
225
+ function performUndo() {
226
+ const previousState = undoRedoManager.undo();
227
+ restoreEditorState(previousState);
228
+ return undoRedoManager.canUndo();
229
+ }
230
+
231
+ // Perform redo
232
+ function performRedo() {
233
+ const nextState = undoRedoManager.redo();
234
+ restoreEditorState(nextState);
235
+ return undoRedoManager.canRedo();
236
+ }
237
+
238
+ // Save initial state
239
+ saveEditorState();
240
+
241
+ // Save state on input with debounce
242
+ const debouncedSaveState = debounce(saveEditorState, 500);
243
+ textarea.addEventListener('input', function(e) {
244
+ if (!undoRedoManager.isUndoRedoAction) {
245
+ debouncedSaveState();
246
+ }
247
+ });
248
+
249
+ // Keyboard shortcuts for Undo/Redo
250
+ textarea.addEventListener('keydown', function(e) {
251
+ // Ctrl+Z or Cmd+Z for Undo
252
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
253
+ e.preventDefault();
254
+ performUndo();
255
+ return false;
256
+ }
257
+
258
+ // Ctrl+Y or Cmd+Shift+Z for Redo
259
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
260
+ e.preventDefault();
261
+ performRedo();
262
+ return false;
263
+ }
264
+ });
265
+
266
+ // Make undo/redo functions globally accessible
267
+ window.editorUndo = performUndo;
268
+ window.editorRedo = performRedo;
269
+ window.canUndo = () => undoRedoManager.canUndo();
270
+ window.canRedo = () => undoRedoManager.canRedo();
271
+
145
272
  // フォーム送信時の処理を非同期化
146
273
  const form = textarea.closest('form');
147
274
  if (form) {
@@ -482,7 +609,7 @@
482
609
  // 外部公開
483
610
  window.getContentEditableSelection = getContentEditableSelection;
484
611
 
485
- // Set selection range at specified position (precision enhanced version)
612
+ // Set selection range at specified position (改行を含むinnerTextベースの位置)
486
613
  function setContentEditableSelection(element, start, end) {
487
614
  element.focus();
488
615
 
@@ -492,78 +619,181 @@
492
619
  start = Math.max(0, Math.min(start, fullText.length));
493
620
  end = Math.max(start, Math.min(end, fullText.length));
494
621
 
495
- // Get text nodes
496
- const walker = document.createTreeWalker(
497
- element,
498
- NodeFilter.SHOW_TEXT,
499
- null,
500
- false
501
- );
622
+ // buildLinearTextMapを使用してDOM構造を解析(改行を含む)
623
+ function buildLinearTextMap(container) {
624
+ let textMap = [];
625
+ let currentPos = 0;
502
626
 
503
- const textNodes = [];
504
- let node;
505
- while (node = walker.nextNode()) {
506
- textNodes.push(node);
507
- }
627
+ function processNode(node) {
628
+ if (node.nodeType === Node.TEXT_NODE) {
629
+ const text = node.textContent;
630
+ textMap.push({
631
+ node: node,
632
+ type: 'text',
633
+ start: currentPos,
634
+ end: currentPos + text.length,
635
+ text: text
636
+ });
637
+ currentPos += text.length;
638
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
639
+ if (node.tagName === 'DIV') {
640
+ if (node.children.length === 1 && node.children[0].tagName === 'BR') {
641
+ textMap.push({
642
+ node: node,
643
+ type: 'div-br',
644
+ start: currentPos,
645
+ end: currentPos + 1,
646
+ text: '\n'
647
+ });
648
+ currentPos += 1;
649
+ return;
650
+ } else if (node.innerHTML === '<br>' || node.innerHTML === '<br/>' || node.innerHTML === '') {
651
+ textMap.push({
652
+ node: node,
653
+ type: 'empty-div',
654
+ start: currentPos,
655
+ end: currentPos + 1,
656
+ text: '\n'
657
+ });
658
+ currentPos += 1;
659
+ return;
660
+ }
661
+ } else if (node.tagName === 'BR') {
662
+ textMap.push({
663
+ node: node,
664
+ type: 'br',
665
+ start: currentPos,
666
+ end: currentPos + 1,
667
+ text: '\n'
668
+ });
669
+ currentPos += 1;
670
+ return;
671
+ }
508
672
 
509
- // Fallback for missing text nodes
510
- if (textNodes.length === 0) {
511
- console.warn('No text nodes found, recreating content');
512
- element.textContent = fullText;
673
+ // 子ノードを処理
674
+ for (let child of node.childNodes) {
675
+ processNode(child);
676
+ }
677
+ }
678
+ }
513
679
 
514
- // Get text nodes again
515
- const newWalker = document.createTreeWalker(
516
- element,
517
- NodeFilter.SHOW_TEXT,
518
- null,
519
- false
520
- );
521
- while (node = newWalker.nextNode()) {
522
- textNodes.push(node);
680
+ for (let child of container.childNodes) {
681
+ processNode(child);
523
682
  }
683
+
684
+ return textMap;
524
685
  }
525
686
 
526
- // Calculate cumulative positions of text nodes
527
- let cumulativeOffset = 0;
687
+ const textMap = buildLinearTextMap(element);
688
+
689
+ // startとendの位置に対応するノードとオフセットを見つける
528
690
  let startNode = null, startOffset = 0;
529
691
  let endNode = null, endOffset = 0;
530
692
 
531
- for (let i = 0; i < textNodes.length; i++) {
532
- const textNode = textNodes[i];
533
- const nodeLength = textNode.textContent.length;
534
-
535
- // Find start position
536
- if (!startNode && cumulativeOffset + nodeLength >= start) {
537
- startNode = textNode;
538
- startOffset = start - cumulativeOffset;
693
+ for (let item of textMap) {
694
+ if (!startNode && start >= item.start && start < item.end) {
695
+ // Position is within this item
696
+ if (item.type === 'text') {
697
+ startNode = item.node;
698
+ startOffset = start - item.start;
699
+ } else {
700
+ // Position is at the start of a line break element
701
+ // Set cursor before this element (not after)
702
+ const parent = item.node.parentNode;
703
+ let childIndex = 0;
704
+ for (let i = 0; i < parent.childNodes.length; i++) {
705
+ if (parent.childNodes[i] === item.node) {
706
+ childIndex = i; // Position before this node
707
+ break;
708
+ }
709
+ }
710
+ startNode = parent;
711
+ startOffset = childIndex;
712
+ }
713
+ } else if (!startNode && start === item.end) {
714
+ // Position is at the end boundary of this item
715
+ if (item.type === 'text') {
716
+ // At end of text node
717
+ startNode = item.node;
718
+ startOffset = start - item.start;
719
+ } else {
720
+ // At end of line break element - set cursor after it
721
+ const parent = item.node.parentNode;
722
+ let childIndex = 0;
723
+ for (let i = 0; i < parent.childNodes.length; i++) {
724
+ if (parent.childNodes[i] === item.node) {
725
+ childIndex = i + 1; // Position after this node
726
+ break;
727
+ }
728
+ }
729
+ startNode = parent;
730
+ startOffset = childIndex;
731
+ }
539
732
  }
540
733
 
541
- // Find end position
542
- if (!endNode && cumulativeOffset + nodeLength >= end) {
543
- endNode = textNode;
544
- endOffset = end - cumulativeOffset;
545
- break;
734
+ if (!endNode && end >= item.start && end < item.end) {
735
+ if (item.type === 'text') {
736
+ endNode = item.node;
737
+ endOffset = end - item.start;
738
+ } else {
739
+ // Position is at the start of a line break element
740
+ const parent = item.node.parentNode;
741
+ let childIndex = 0;
742
+ for (let i = 0; i < parent.childNodes.length; i++) {
743
+ if (parent.childNodes[i] === item.node) {
744
+ childIndex = i; // Position before this node
745
+ break;
746
+ }
747
+ }
748
+ endNode = parent;
749
+ endOffset = childIndex;
750
+ }
751
+ } else if (!endNode && end === item.end) {
752
+ if (item.type === 'text') {
753
+ endNode = item.node;
754
+ endOffset = end - item.start;
755
+ } else {
756
+ const parent = item.node.parentNode;
757
+ let childIndex = 0;
758
+ for (let i = 0; i < parent.childNodes.length; i++) {
759
+ if (parent.childNodes[i] === item.node) {
760
+ childIndex = i + 1;
761
+ break;
762
+ }
763
+ }
764
+ endNode = parent;
765
+ endOffset = childIndex;
766
+ }
546
767
  }
547
768
 
548
- cumulativeOffset += nodeLength;
769
+ if (startNode && endNode) break;
549
770
  }
550
771
 
551
- // Enhanced fallback processing
552
- if (!startNode && textNodes.length > 0) {
553
- startNode = textNodes[textNodes.length - 1];
554
- startOffset = Math.min(start, startNode.textContent.length);
555
- }
556
-
557
- if (!endNode && textNodes.length > 0) {
558
- endNode = textNodes[textNodes.length - 1];
559
- endOffset = Math.min(end, endNode.textContent.length);
772
+ // Fallback: 位置が見つからない場合、最後のテキストノードの末尾に設定
773
+ if (!startNode || !endNode) {
774
+ const textItems = textMap.filter(i => i.type === 'text');
775
+ if (textItems.length > 0) {
776
+ const lastTextItem = textItems[textItems.length - 1];
777
+ if (!startNode) {
778
+ startNode = lastTextItem.node;
779
+ startOffset = lastTextItem.node.textContent.length;
780
+ }
781
+ if (!endNode) {
782
+ endNode = lastTextItem.node;
783
+ endOffset = lastTextItem.node.textContent.length;
784
+ }
785
+ }
560
786
  }
561
787
 
562
788
  if (startNode && endNode) {
563
789
  try {
564
- // Safe boundary setting
565
- startOffset = Math.max(0, Math.min(startOffset, startNode.textContent.length));
566
- endOffset = Math.max(0, Math.min(endOffset, endNode.textContent.length));
790
+ // Only clamp offset for text nodes, not element nodes
791
+ if (startNode.nodeType === Node.TEXT_NODE) {
792
+ startOffset = Math.max(0, Math.min(startOffset, startNode.textContent.length));
793
+ }
794
+ if (endNode.nodeType === Node.TEXT_NODE) {
795
+ endOffset = Math.max(0, Math.min(endOffset, endNode.textContent.length));
796
+ }
567
797
 
568
798
  const range = document.createRange();
569
799
  range.setStart(startNode, startOffset);
@@ -575,7 +805,6 @@
575
805
 
576
806
  } catch (e) {
577
807
  console.error('Selection setting failed:', e);
578
- // Final fallback
579
808
  try {
580
809
  const range = document.createRange();
581
810
  range.selectNodeContents(element);
@@ -730,44 +959,97 @@
730
959
  }
731
960
  }
732
961
 
962
+ // ノードがtextMapにない場合(親コンテナなど)、子ノードから計算
963
+ if (node === container && node.nodeType === Node.ELEMENT_NODE) {
964
+ let calculatedOffset = 0;
965
+ for (let i = 0; i < offset && i < node.childNodes.length; i++) {
966
+ const child = node.childNodes[i];
967
+ if (child.nodeType === Node.TEXT_NODE) {
968
+ calculatedOffset += child.textContent.length;
969
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
970
+ // Check if this child is in the textMap
971
+ const childItem = textMap.find(item => item.node === child);
972
+ if (childItem) {
973
+ calculatedOffset += (childItem.end - childItem.start);
974
+ }
975
+ }
976
+ }
977
+ return calculatedOffset;
978
+ }
979
+
733
980
  return 0;
734
981
  }
735
982
 
736
983
  function selectLineNumberAndCharIndex(beginEndLenStrings, beginCharIndex, endCharIndex) {
737
984
  let retBeginLine = 0, retBeginCharIndex = 0;
738
985
  let retEndLine = 0, retEndCharIndex = 0;
739
- let emptyLineCount = 0;
740
986
 
987
+ // Find line for beginCharIndex - handle boundary ambiguity by preferring non-empty lines
988
+ let beginMatches = [];
741
989
  for (let i = 0; i < beginEndLenStrings.length; i++) {
742
990
  const line = beginEndLenStrings[i];
743
-
744
- if (line.str === "⹉") {
745
- emptyLineCount++;
991
+ if (beginCharIndex >= line.begin && beginCharIndex <= line.end) {
992
+ beginMatches.push({
993
+ line: i,
994
+ char: beginCharIndex - line.begin,
995
+ isEmpty: line.str === "⹉",
996
+ isAtBoundary: beginCharIndex === line.begin || beginCharIndex === line.end
997
+ });
746
998
  }
999
+ }
747
1000
 
748
- if (beginCharIndex >= line.begin && beginCharIndex <= line.end) {
749
- retBeginLine = i;
750
- retBeginCharIndex = beginCharIndex - line.begin + emptyLineCount;
751
- break;
1001
+ console.log('selectLineNumberAndCharIndex DEBUG:');
1002
+ console.log(' beginCharIndex:', beginCharIndex);
1003
+ console.log(' beginMatches:', JSON.stringify(beginMatches, null, 2));
1004
+
1005
+ // Save to debug info for testing
1006
+ if (typeof window !== 'undefined' && window._debugInfo) {
1007
+ window._debugInfo.selectDebug = {
1008
+ beginCharIndex: beginCharIndex,
1009
+ beginMatches: beginMatches
1010
+ };
1011
+ }
1012
+
1013
+ // If multiple matches, prefer non-empty lines
1014
+ if (beginMatches.length > 1) {
1015
+ const nonEmptyMatches = beginMatches.filter(m => !m.isEmpty);
1016
+ if (nonEmptyMatches.length > 0) {
1017
+ beginMatches = nonEmptyMatches;
752
1018
  }
753
1019
  }
754
1020
 
755
- emptyLineCount = 0;
1021
+ if (beginMatches.length > 0) {
1022
+ retBeginLine = beginMatches[0].line;
1023
+ retBeginCharIndex = beginMatches[0].char;
1024
+ }
756
1025
 
1026
+ // Find line for endCharIndex - handle boundary ambiguity by preferring non-empty lines
1027
+ let endMatches = [];
757
1028
  for (let i = 0; i < beginEndLenStrings.length; i++) {
758
1029
  const line = beginEndLenStrings[i];
759
-
760
- if (line.str === "⹉") {
761
- emptyLineCount++;
1030
+ if (endCharIndex >= line.begin && endCharIndex <= line.end) {
1031
+ endMatches.push({
1032
+ line: i,
1033
+ char: endCharIndex - line.begin,
1034
+ isEmpty: line.str === "⹉",
1035
+ isAtBoundary: endCharIndex === line.begin || endCharIndex === line.end
1036
+ });
762
1037
  }
1038
+ }
763
1039
 
764
- if (endCharIndex >= line.begin && endCharIndex <= line.end) {
765
- retEndLine = i;
766
- retEndCharIndex = endCharIndex - line.begin + emptyLineCount;
767
- break;
1040
+ // If multiple matches, prefer non-empty lines
1041
+ if (endMatches.length > 1) {
1042
+ const nonEmptyMatches = endMatches.filter(m => !m.isEmpty);
1043
+ if (nonEmptyMatches.length > 0) {
1044
+ endMatches = nonEmptyMatches;
768
1045
  }
769
1046
  }
770
1047
 
1048
+ if (endMatches.length > 0) {
1049
+ retEndLine = endMatches[0].line;
1050
+ retEndCharIndex = endMatches[0].char;
1051
+ }
1052
+
771
1053
  return { begin: { line: retBeginLine, char: retBeginCharIndex }, end: { line: retEndLine, char: retEndCharIndex } };
772
1054
  }
773
1055
 
@@ -776,9 +1058,15 @@
776
1058
  for (let i = 0; i < beginEndLenStrings.length; i++) {
777
1059
  const line = beginEndLenStrings[i];
778
1060
 
779
- // 改行行は変更せずそのまま保持
1061
+ // 空行(改行行)の処理
780
1062
  if (line.str === "⹉") {
781
- newLines.push(line.str);
1063
+ // カーソルが空行にある場合、before + after を挿入
1064
+ if (i === targetTextPosition.begin.line && i === targetTextPosition.end.line) {
1065
+ newLines.push(before + after);
1066
+ } else {
1067
+ // 空行は変更せずそのまま保持
1068
+ newLines.push(line.str);
1069
+ }
782
1070
  continue;
783
1071
  }
784
1072
 
@@ -825,18 +1113,21 @@
825
1113
  }
826
1114
 
827
1115
  function convertToInnerHtml(newLines) {
1116
+ // ブラウザのネイティブ構造に合わせて、<br>タグを使用した平坦な構造を生成
828
1117
  let html = "";
829
1118
  for (let i = 0; i < newLines.length; i++) {
830
- if (i === 0) {
831
- html += newLines[i].split("⹉").join("");
1119
+ let insert = newLines[i];
1120
+ if (i > 0) {
1121
+ // 行の区切りとして<br>を追加
1122
+ html += "<br>";
1123
+ }
1124
+ if (insert === "⹉") {
1125
+ // 空行の場合は何も追加しない(<br>タグのみで表現)
1126
+ // noop
832
1127
  } else {
833
- let insert = newLines[i];
834
- if (insert === "⹉") {
835
- insert = insert.split("⹉").join("<br>");
836
- } else {
837
- insert = insert.split("⹉").join("");
838
- }
839
- html += "<div>" + insert + "</div>";
1128
+ // 通常の行の場合はテキストを追加
1129
+ insert = insert.split("⹉").join("");
1130
+ html += insert;
840
1131
  }
841
1132
  }
842
1133
  return html;
@@ -908,8 +1199,6 @@
908
1199
  }
909
1200
  }
910
1201
 
911
- const MAX_CACHE_SIZE = 500;
912
-
913
1202
  function analyzeHtml(target, isCountEmptyDiv = false) {
914
1203
 
915
1204
  const cacheKey = `${target}_${isCountEmptyDiv}`;
@@ -918,68 +1207,121 @@
918
1207
  return analyzeHtmlCache.get(cacheKey);
919
1208
  }
920
1209
 
1210
+ // Helper function to strip HTML tags
1211
+ function stripHtmlTags(html) {
1212
+ return html.replace(/<[^>]*>/g, '');
1213
+ }
1214
+
921
1215
  let lines = [];
922
- let remain = target;
923
1216
 
924
- while (remain.length > 0) {
925
- // <div><br></div> パターン(改行)- 常に維持
926
- if (remain.startsWith("<div><br></div>")) {
927
- lines.push("⹉");
928
- remain = remain.substring(15);
929
- continue;
930
- }
1217
+ // Check if this is div-based structure or br-based structure
1218
+ const hasDivStructure = target.startsWith("<div>") || target.startsWith("<div ");
931
1219
 
932
- // <div></div> パターン(空のdiv)- isCountEmptyDivで制御
933
- if (remain.startsWith("<div></div>")) {
934
- if (isCountEmptyDiv) {
1220
+ if (!hasDivStructure && target.includes("<br>")) {
1221
+ // Br-based structure (flat): Split by <br> tags
1222
+ const segments = target.split("<br>");
1223
+ for (let segment of segments) {
1224
+ const textContent = stripHtmlTags(segment);
1225
+ if (textContent === "") {
935
1226
  lines.push("⹉");
1227
+ } else {
1228
+ lines.push(textContent);
936
1229
  }
937
- remain = remain.substring(11);
938
- continue;
939
1230
  }
1231
+ } else {
1232
+ // Div-based structure: Parse divs
1233
+ let remain = target;
940
1234
 
941
- // <div>内容</div> パターン
942
- const divMatch = remain.match(/^<div>([^<]*)<\/div>/);
943
- if (divMatch) {
944
- lines.push(divMatch[1]);
945
- remain = remain.substring(divMatch[0].length);
946
- continue;
947
- }
1235
+ while (remain.length > 0) {
1236
+ // <div><br></div> パターン(改行)- 常に維持
1237
+ if (remain.startsWith("<div><br></div>")) {
1238
+ lines.push("⹉");
1239
+ remain = remain.substring(15);
1240
+ continue;
1241
+ }
948
1242
 
949
- // <br> パターン(単体の改行)
950
- if (remain.startsWith("<br>")) {
951
- lines.push("⹉");
952
- remain = remain.substring(4);
953
- continue;
954
- }
1243
+ // <div></div> パターン(空のdiv)- isCountEmptyDivで制御
1244
+ if (remain.startsWith("<div></div>")) {
1245
+ if (isCountEmptyDiv) {
1246
+ lines.push("⹉");
1247
+ }
1248
+ remain = remain.substring(11);
1249
+ continue;
1250
+ }
955
1251
 
956
- // <div> の開始を探す
957
- const divIndex = remain.indexOf("<div>");
958
- if (divIndex > 0) {
959
- lines.push(remain.substring(0, divIndex));
960
- remain = remain.substring(divIndex);
961
- continue;
962
- }
1252
+ // <div>内容</div> パターン(ネストされたタグも考慮)
1253
+ if (remain.startsWith("<div>")) {
1254
+ // Find the matching </div> by counting depth
1255
+ let depth = 0;
1256
+ let i = 0;
1257
+ let foundMatch = false;
1258
+
1259
+ while (i < remain.length) {
1260
+ if (remain.substring(i, i + 5) === "<div>") {
1261
+ depth++;
1262
+ i += 5;
1263
+ } else if (remain.substring(i, i + 6) === "</div>") {
1264
+ depth--;
1265
+ if (depth === 0) {
1266
+ // Found the matching closing tag
1267
+ const content = remain.substring(5, i); // Content between <div> and </div>
1268
+ // Strip all HTML tags from content to get text only
1269
+ const textContent = stripHtmlTags(content);
1270
+ lines.push(textContent);
1271
+ remain = remain.substring(i + 6);
1272
+ foundMatch = true;
1273
+ break;
1274
+ }
1275
+ i += 6;
1276
+ } else {
1277
+ i++;
1278
+ }
1279
+ }
963
1280
 
964
- // その他のタグまたは残りのテキスト
965
- const nextTagIndex = remain.indexOf("<");
966
- if (nextTagIndex === -1) {
967
- if (remain.trim()) {
968
- lines.push(remain);
1281
+ if (foundMatch) {
1282
+ continue;
1283
+ }
969
1284
  }
970
- break;
971
- }
972
1285
 
973
- if (nextTagIndex > 0) {
974
- lines.push(remain.substring(0, nextTagIndex));
975
- remain = remain.substring(nextTagIndex);
976
- } else {
977
- const tagEnd = remain.indexOf(">");
978
- if (tagEnd !== -1) {
979
- remain = remain.substring(tagEnd + 1);
980
- } else {
1286
+ // <div> の開始を探す
1287
+ const divIndex = remain.indexOf("<div>");
1288
+ if (divIndex > 0) {
1289
+ // Strip HTML tags from the text before <div>
1290
+ const textContent = stripHtmlTags(remain.substring(0, divIndex));
1291
+ if (textContent) {
1292
+ lines.push(textContent);
1293
+ }
1294
+ remain = remain.substring(divIndex);
1295
+ continue;
1296
+ }
1297
+
1298
+ // その他のタグまたは残りのテキスト
1299
+ const nextTagIndex = remain.indexOf("<");
1300
+ if (nextTagIndex === -1) {
1301
+ // No more tags, strip any remaining HTML and add as text
1302
+ const textContent = stripHtmlTags(remain);
1303
+ if (textContent.trim()) {
1304
+ lines.push(textContent);
1305
+ }
981
1306
  break;
982
1307
  }
1308
+
1309
+ if (nextTagIndex > 0) {
1310
+ // Text before next tag
1311
+ const textContent = stripHtmlTags(remain.substring(0, nextTagIndex));
1312
+ if (textContent) {
1313
+ lines.push(textContent);
1314
+ }
1315
+ remain = remain.substring(nextTagIndex);
1316
+ } else {
1317
+ // Skip the tag
1318
+ const tagEnd = remain.indexOf(">");
1319
+ if (tagEnd !== -1) {
1320
+ remain = remain.substring(tagEnd + 1);
1321
+ } else {
1322
+ break;
1323
+ }
1324
+ }
983
1325
  }
984
1326
  }
985
1327
 
@@ -1010,8 +1352,14 @@
1010
1352
  const isContentEditable = textarea.contentEditable === 'true' ||
1011
1353
  textarea.getAttribute('contenteditable') === 'true';
1012
1354
 
1355
+ console.log('=== HTML Debug ===');
1356
+ console.log('textarea.innerHTML:', textarea.innerHTML);
1357
+
1013
1358
  let lines = analyzeHtml(textarea.innerHTML.replace('\n \n ', ''));
1014
1359
 
1360
+ console.log('lines from analyzeHtml:', JSON.stringify(lines, null, 2));
1361
+ console.log('==================');
1362
+
1015
1363
  // === 修正部分:DOM構造に基づいたテキスト表現を構築 ===
1016
1364
  let domBasedText = '';
1017
1365
  for (let i = 0; i < lines.length; i++) {
@@ -1023,6 +1371,9 @@
1023
1371
  }
1024
1372
 
1025
1373
  // beginEndLenStringsをDOM構造ベースで構築
1374
+ // br-based構造かdiv-based構造かを判定
1375
+ const isBrBased = !textarea.innerHTML.startsWith("<div>") && !textarea.innerHTML.startsWith("<div ") && textarea.innerHTML.includes("<br>");
1376
+
1026
1377
  let offset = 0;
1027
1378
  let beginEndLenStrings = [];
1028
1379
  for (let i = 0; i < lines.length; i++) {
@@ -1030,7 +1381,9 @@
1030
1381
  let actualLength;
1031
1382
 
1032
1383
  if (line === "⹉") {
1033
- actualLength = 1; // 改行として1文字
1384
+ // br-basedの場合、空行は長さ0(<br>が改行を表現している)
1385
+ // div-basedの場合は長さ1
1386
+ actualLength = isBrBased ? 0 : 1;
1034
1387
  } else {
1035
1388
  actualLength = line.length;
1036
1389
  }
@@ -1042,6 +1395,11 @@
1042
1395
  str: line,
1043
1396
  });
1044
1397
  offset += actualLength;
1398
+
1399
+ // br-based構造の場合、各行(最後以外)の後に<br>セパレータがあるので+1
1400
+ if (isBrBased && i < lines.length - 1) {
1401
+ offset += 1; // <br>セパレータ分
1402
+ }
1045
1403
  }
1046
1404
  // ===================================================
1047
1405
 
@@ -1063,10 +1421,9 @@
1063
1421
  let selectLineMode = false;
1064
1422
  let selectedLinePos;
1065
1423
 
1066
- if (!selection.rangeCount || selection.isCollapsed) {
1067
- // 行選択モード
1068
- selectLineMode = true;
1069
- selectedLinePos = getCurrentLineIndex(beginEndLenStrings);
1424
+ if (!selection.rangeCount) {
1425
+ console.warn('No selection range');
1426
+ return;
1070
1427
  }
1071
1428
 
1072
1429
  const range = selection.getRangeAt(0);
@@ -1079,11 +1436,19 @@
1079
1436
  const startOffset = getOffsetInContainer(textarea, range.startContainer, range.startOffset);
1080
1437
  const endOffset = getOffsetInContainer(textarea, range.endContainer, range.endOffset);
1081
1438
 
1082
- // === デバッグ出力(削除可能)===
1083
- // console.log('DOM-based text:', JSON.stringify(domBasedText));
1084
- // console.log('beginEndLenStrings:', beginEndLenStrings);
1085
- // console.log('Selection offsets:', startOffset, endOffset);
1086
- // console.log('Selected text should be:', JSON.stringify(domBasedText.substring(startOffset, endOffset)));
1439
+ // === デバッグ出力 ===
1440
+ console.log('=== insertMarkdown Debug ===');
1441
+ console.log('DOM-based text:', JSON.stringify(domBasedText));
1442
+ console.log('Selection offsets - start:', startOffset, 'end:', endOffset);
1443
+ console.log('Selection containers - start:', range.startContainer, 'end:', range.endContainer);
1444
+ console.log('Inserted text will be:', before + after);
1445
+
1446
+ // Save debug info to DOM for testing
1447
+ window._debugInfo = {
1448
+ domBasedText: domBasedText,
1449
+ startOffset: startOffset,
1450
+ endOffset: endOffset
1451
+ };
1087
1452
  // =============================
1088
1453
 
1089
1454
  const startPos = getLineAndCharIndex(textarea, startOffset);
@@ -1091,65 +1456,54 @@
1091
1456
 
1092
1457
  const selectedTextContent = selection.toString();
1093
1458
 
1459
+ console.log('=== Line Position Debug ===');
1460
+ console.log('beginEndLenStrings:', JSON.stringify(beginEndLenStrings, null, 2));
1461
+ console.log('startOffset:', startOffset, 'endOffset:', endOffset);
1462
+ console.log('selectedTextContent:', JSON.stringify(selectedTextContent));
1463
+ console.log('selectLineMode:', selectLineMode);
1464
+
1094
1465
  const targetTextPosition = selectLineNumberAndCharIndex(beginEndLenStrings, startOffset, endOffset);
1095
1466
 
1467
+ console.log('targetTextPosition:', JSON.stringify(targetTextPosition, null, 2));
1468
+
1096
1469
  const newLines = !selectLineMode
1097
1470
  ? replaceLineNumberAndCharIndex(beginEndLenStrings, targetTextPosition, before, after)
1098
1471
  : replaceLine(beginEndLenStrings, selectedLinePos.line, before, after);
1099
1472
 
1473
+ console.log('newLines:', JSON.stringify(newLines, null, 2));
1474
+ console.log('newLines.length:', newLines.length);
1475
+ console.log('newLines breakdown:');
1476
+ for (let i = 0; i < newLines.length; i++) {
1477
+ console.log(` Line ${i}: ${JSON.stringify(newLines[i])}`);
1478
+ }
1479
+ console.log('==========================');
1480
+
1481
+ // Save more debug info
1482
+ window._debugInfo.beginEndLenStrings = beginEndLenStrings;
1483
+ window._debugInfo.targetTextPosition = targetTextPosition;
1484
+ window._debugInfo.newLines = newLines;
1485
+
1100
1486
  const newFullHTML = convertToInnerHtml(newLines);
1101
1487
 
1102
1488
  if (isContentEditable) {
1103
1489
  textarea.innerHTML = newFullHTML;
1104
1490
  textarea.focus();
1105
1491
 
1106
- // 挿入されたテキストノードを直接探してカーソルを設定
1107
- const insertedText = selectedTextContent.length > 0 ?
1108
- (before + selectedTextContent + after) :
1109
- (before + after);
1110
-
1111
- // 新しく挿入されたテキストの終端を探す
1112
- const walker = document.createTreeWalker(
1113
- textarea,
1114
- NodeFilter.SHOW_TEXT,
1115
- null,
1116
- false
1117
- );
1118
-
1119
- let foundNode = null;
1120
- let foundOffset = 0;
1121
- let node;
1122
-
1123
- while (node = walker.nextNode()) {
1124
- if (node.textContent.includes(insertedText)) {
1125
- // 挿入されたテキストを含むノードを発見
1126
- const textIndex = node.textContent.indexOf(insertedText);
1127
- if (textIndex !== -1) {
1128
- foundNode = node;
1129
- foundOffset = textIndex + insertedText.length;
1130
- break;
1131
- }
1132
- }
1133
- }
1492
+ // カーソル位置を計算(空文字選択時はbeforeの直後、テキスト選択時は挿入テキストの最後)
1493
+ const cursorOffsetInInsertedText = selectedTextContent.length > 0 ?
1494
+ (before.length + selectedTextContent.length + after.length) :
1495
+ before.length;
1134
1496
 
1135
- if (foundNode) {
1136
- // 見つかったテキストノード内にカーソルを設定
1137
- const range = document.createRange();
1138
- range.setStart(foundNode, Math.min(foundOffset, foundNode.textContent.length));
1139
- range.collapse(true);
1497
+ const targetPosition = startOffset + cursorOffsetInInsertedText;
1140
1498
 
1141
- const selection = window.getSelection();
1142
- selection.removeAllRanges();
1143
- selection.addRange(range);
1144
- } else {
1145
- // フォールバック: 従来の方法
1146
- const newDomText = buildDomBasedText(textarea);
1147
- const targetPosition = Math.min(
1148
- startOffset + insertedText.length,
1149
- newDomText.length
1150
- );
1151
- setContentEditableSelection(textarea, targetPosition, targetPosition);
1152
- }
1499
+ console.log('=== Cursor Position Debug ===');
1500
+ console.log('startOffset:', startOffset);
1501
+ console.log('cursorOffsetInInsertedText:', cursorOffsetInInsertedText);
1502
+ console.log('targetPosition:', targetPosition);
1503
+ console.log('============================');
1504
+
1505
+ // setContentEditableSelection関数を使用(innerTextベースで改行を正しく扱う)
1506
+ setContentEditableSelection(textarea, targetPosition, targetPosition);
1153
1507
  }
1154
1508
 
1155
1509
  // Fire input event
@@ -1160,6 +1514,10 @@
1160
1514
  }
1161
1515
  };
1162
1516
 
1517
+ // Expose setContentEditableSelection for testing
1518
+ window.setContentEditableSelection = setContentEditableSelection;
1519
+ window._debugGetOffsetInContainer = getOffsetInContainer;
1520
+
1163
1521
  function buildDomBasedText(element) {
1164
1522
  const lines = analyzeHtml(element.innerHTML);
1165
1523
  let domText = '';
@@ -28,7 +28,8 @@ module PonkotsuMdEditor
28
28
  attribute = attribute[:attribute] if attribute.is_a?(Hash)
29
29
  content = content[:attribute] if content.is_a?(Hash)
30
30
  options = options[:options] if options.is_a?(Hash)
31
- render "ponkotsu_md_editor/editor", locals: { attribute: attribute, content: content, form: form, options: options }
31
+ render "ponkotsu_md_editor/editor",
32
+ locals: { attribute: attribute, content: content, form: form, options: options }
32
33
  end
33
34
  end
34
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PonkotsuMdEditor
4
- VERSION = "0.2.38"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ponkotsu-md-editor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.38
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dhq_boiler
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 8.0.2
18
+ version: '8.1'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 8.0.2
25
+ version: '8.1'
26
26
  description: There is a bug in Chrome where entering large amounts of text into a
27
27
  textarea element causes significant slowness (https://issues.chromium.org/issues/341564372).
28
28
  This gem serves as a countermeasure for that issue.
@@ -32,7 +32,9 @@ executables: []
32
32
  extensions: []
33
33
  extra_rdoc_files: []
34
34
  files:
35
+ - ".ruby-version"
35
36
  - CHANGELOG.md
37
+ - E2E_TESTING.md
36
38
  - LICENSE.txt
37
39
  - README.md
38
40
  - Rakefile
@@ -64,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
64
66
  requirements:
65
67
  - - ">="
66
68
  - !ruby/object:Gem::Version
67
- version: 3.4.5
69
+ version: 3.0.0
68
70
  required_rubygems_version: !ruby/object:Gem::Requirement
69
71
  requirements:
70
72
  - - ">="