neiki-editor 2.9.5 → 2.10.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 CHANGED
@@ -11,7 +11,7 @@
11
11
  <img src="https://img.shields.io/badge/css-%23663399.svg?style=for-the-badge&logo=css&logoColor=white" alt="CSS">
12
12
  <br>
13
13
  <img src="https://img.shields.io/badge/License-AGPL--3.0-2563EB?style=for-the-badge&logo=open-source-initiative&logoColor=white&labelColor=000F15&logoWidth=20" alt="License">
14
- <img src="https://img.shields.io/badge/Version-2.9.5-2563EB?style=for-the-badge&logo=semantic-release&logoColor=white&labelColor=000F15&logoWidth=20" alt="Version">
14
+ <img src="https://img.shields.io/badge/Version-2.10.0-2563EB?style=for-the-badge&logo=semantic-release&logoColor=white&labelColor=000F15&logoWidth=20" alt="Version">
15
15
  </p>
16
16
 
17
17
  <p align="center">
@@ -62,7 +62,7 @@ Add this single line — CSS is included automatically, always the **latest vers
62
62
  #### Pin a specific version
63
63
 
64
64
  ```html
65
- <script src="https://cdn.neikiri.dev/neiki-editor/2.9.5/neiki-editor.min.js"></script>
65
+ <script src="https://cdn.neikiri.dev/neiki-editor/2.10.0/neiki-editor.min.js"></script>
66
66
  ```
67
67
 
68
68
  #### Load CSS and JS separately
@@ -73,8 +73,8 @@ Add this single line — CSS is included automatically, always the **latest vers
73
73
  <script src="https://cdn.neikiri.dev/neiki-editor/neiki-editor.js"></script>
74
74
 
75
75
  <!-- Or pinned -->
76
- <link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-editor/2.9.5/neiki-editor.css">
77
- <script src="https://cdn.neikiri.dev/neiki-editor/2.9.5/neiki-editor.js"></script>
76
+ <link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-editor/2.10.0/neiki-editor.css">
77
+ <script src="https://cdn.neikiri.dev/neiki-editor/2.10.0/neiki-editor.js"></script>
78
78
  ```
79
79
 
80
80
  #### Alternative CDN — jsDelivr
@@ -84,15 +84,15 @@ Add this single line — CSS is included automatically, always the **latest vers
84
84
  <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@latest/dist/neiki-editor.min.js"></script>
85
85
 
86
86
  <!-- Pinned -->
87
- <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.9.5/dist/neiki-editor.min.js"></script>
87
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.10.0/dist/neiki-editor.min.js"></script>
88
88
 
89
89
  <!-- Separate files (latest) -->
90
90
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@latest/dist/neiki-editor.css">
91
91
  <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@latest/dist/neiki-editor.js"></script>
92
92
 
93
93
  <!-- Separate files (pinned) -->
94
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.9.5/dist/neiki-editor.css">
95
- <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.9.5/dist/neiki-editor.js"></script>
94
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.10.0/dist/neiki-editor.css">
95
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.10.0/dist/neiki-editor.js"></script>
96
96
  ```
97
97
 
98
98
  #### Package Manager
@@ -147,10 +147,10 @@ const editor = new NeikiEditor('#editor', {
147
147
  language: 'en', // 'en', 'cs', or custom via addTranslation()
148
148
  translations: null, // custom translation keys (merged with built-in)
149
149
  autosaveKey: null, // optional custom localStorage scope for autosave
150
- custom_class: null, // optional custom CSS class for the content area
150
+ customClass: null, // optional custom CSS class for the content area
151
151
  toolbar: [
152
152
  'viewCode', 'undo', 'redo', 'findReplace', '|',
153
- 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'removeFormat', '|',
153
+ 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'code', 'removeFormat', '|',
154
154
  'heading', 'fontFamily', 'fontSize', '|',
155
155
  'foreColor', 'backColor', '|',
156
156
  'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
@@ -193,7 +193,7 @@ const editor = new NeikiEditor('#editor', {
193
193
  | `onReady` | `function\|null` | `null` | Callback when editor is ready |
194
194
  | `showHelp` | `boolean` | `true` | Show Help button in More menu (⋯) |
195
195
  | `imageUploadHandler` | `function\|null` | `null` | Async callback `(file) => Promise<url>` for uploading images to a server/CDN instead of base64 |
196
- | `custom_class` | `string\|null` | `null` | Custom CSS class appended to the editor content area (`neiki-content`) |
196
+ | `customClass` | `string\|null` | `null` | Custom CSS class appended to the editor content area (`neiki-content`) |
197
197
 
198
198
  ---
199
199
 
@@ -211,6 +211,7 @@ Use the `toolbar` array to customize which buttons appear and in what order. Use
211
211
  | `strikethrough` | Strikethrough text |
212
212
  | `subscript` | Subscript text |
213
213
  | `superscript` | Superscript text |
214
+ | `code` | Toggle code formatting — inline `<code>` or `<pre><code>` block |
214
215
  | `removeFormat` | Remove all formatting |
215
216
 
216
217
  > **Note:** When no text is selected, formatting commands (including Remove Formatting) automatically expand to the word at the cursor position.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * NeikiEditor - Production-Ready WYSIWYG Rich Text Editor
3
3
  * CSS Stylesheet
4
- * Version: 2.9.5
4
+ * Version: 2.10.0
5
5
  */
6
6
 
7
7
  /* ============================================
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * NeikiEditor - A Modern WYSIWYG Editor
3
- * Version: 2.9.5
3
+ * Version: 2.10.0
4
4
  *
5
5
  * A lightweight, feature-rich text editor with support for:
6
6
  * - Rich text formatting (bold, italic, underline, etc.)
@@ -63,6 +63,7 @@
63
63
  'toolbar.autosave': 'Toggle Autosave',
64
64
  'toolbar.themeToggle': 'Toggle Theme',
65
65
  'toolbar.print': 'Print',
66
+ 'toolbar.code': 'Code',
66
67
  'toolbar.insert': 'Insert',
67
68
  'toolbar.moreOptions': 'More options',
68
69
  'toolbar.decreaseFontSize': 'Decrease font size',
@@ -219,6 +220,7 @@
219
220
  'toolbar.autosave': 'Auto. ukládání',
220
221
  'toolbar.themeToggle': 'Přepnout motiv',
221
222
  'toolbar.print': 'Tisk',
223
+ 'toolbar.code': 'Kód',
222
224
  'toolbar.insert': 'Vložit',
223
225
  'toolbar.moreOptions': 'Další možnosti',
224
226
  'toolbar.decreaseFontSize': 'Zmenšit písmo',
@@ -369,6 +371,7 @@
369
371
  'toolbar.autosave': '自动保存',
370
372
  'toolbar.themeToggle': '切换主题',
371
373
  'toolbar.print': '打印',
374
+ 'toolbar.code': '代码',
372
375
  'toolbar.insert': '插入',
373
376
  'toolbar.moreOptions': '更多选项',
374
377
  'toolbar.decreaseFontSize': '缩小字体',
@@ -494,6 +497,7 @@
494
497
  'toolbar.autosave': 'Guardado automático',
495
498
  'toolbar.themeToggle': 'Cambiar tema',
496
499
  'toolbar.print': 'Imprimir',
500
+ 'toolbar.code': 'Código',
497
501
  'toolbar.insert': 'Insertar',
498
502
  'toolbar.moreOptions': 'Más opciones',
499
503
  'toolbar.decreaseFontSize': 'Reducir tamaño de fuente',
@@ -619,6 +623,7 @@
619
623
  'toolbar.autosave': 'Automatisch speichern',
620
624
  'toolbar.themeToggle': 'Design wechseln',
621
625
  'toolbar.print': 'Drucken',
626
+ 'toolbar.code': 'Code',
622
627
  'toolbar.insert': 'Einfügen',
623
628
  'toolbar.moreOptions': 'Weitere Optionen',
624
629
  'toolbar.decreaseFontSize': 'Schrift verkleinern',
@@ -744,6 +749,7 @@
744
749
  'toolbar.autosave': 'Sauvegarde automatique',
745
750
  'toolbar.themeToggle': 'Changer de thème',
746
751
  'toolbar.print': 'Imprimer',
752
+ 'toolbar.code': 'Code',
747
753
  'toolbar.insert': 'Insérer',
748
754
  'toolbar.moreOptions': 'Plus d\'options',
749
755
  'toolbar.decreaseFontSize': 'Réduire la taille de police',
@@ -869,6 +875,7 @@
869
875
  'toolbar.autosave': 'Salvamento automático',
870
876
  'toolbar.themeToggle': 'Alternar tema',
871
877
  'toolbar.print': 'Imprimir',
878
+ 'toolbar.code': 'Código',
872
879
  'toolbar.insert': 'Inserir',
873
880
  'toolbar.moreOptions': 'Mais opções',
874
881
  'toolbar.decreaseFontSize': 'Diminuir fonte',
@@ -994,6 +1001,7 @@
994
1001
  'toolbar.autosave': '自動保存',
995
1002
  'toolbar.themeToggle': 'テーマ切替',
996
1003
  'toolbar.print': '印刷',
1004
+ 'toolbar.code': 'コード',
997
1005
  'toolbar.insert': '挿入',
998
1006
  'toolbar.moreOptions': 'その他',
999
1007
  'toolbar.decreaseFontSize': 'フォント縮小',
@@ -1112,7 +1120,7 @@
1112
1120
  const DEFAULT_CONFIG = {
1113
1121
  toolbar: [
1114
1122
  'viewCode', 'undo', 'redo', 'findReplace', '|',
1115
- 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'removeFormat', '|',
1123
+ 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'code', 'removeFormat', '|',
1116
1124
  'heading', 'fontFamily', 'fontSize', '|',
1117
1125
  'foreColor', 'backColor', '|',
1118
1126
  'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
@@ -1139,7 +1147,7 @@
1139
1147
  onReady: null,
1140
1148
  showHelp: true,
1141
1149
  imageUploadHandler: null,
1142
- custom_class: null
1150
+ customClass: null
1143
1151
  };
1144
1152
 
1145
1153
  const TOOLBAR_ITEMS = {
@@ -1170,6 +1178,7 @@
1170
1178
  horizontalRule: { icon: 'minus', titleKey: 'toolbar.horizontalRule', command: 'insertHorizontalRule' },
1171
1179
  subscript: { icon: 'subscript', titleKey: 'toolbar.subscript', command: 'subscript' },
1172
1180
  superscript: { icon: 'superscript', titleKey: 'toolbar.superscript', command: 'superscript' },
1181
+ code: { icon: 'code-inline', titleKey: 'toolbar.code', command: 'toggleCode' },
1173
1182
  removeFormat: { icon: 'eraser', titleKey: 'toolbar.removeFormat', command: 'removeFormat' },
1174
1183
  findReplace: { icon: 'search', titleKey: 'toolbar.findReplace', command: 'findReplace', modal: true },
1175
1184
  emoji: { icon: 'emoji', titleKey: 'toolbar.emoji', command: 'emoji', picker: 'emoji' },
@@ -1362,16 +1371,22 @@
1362
1371
 
1363
1372
  const currentParent = () => stack[stack.length - 1];
1364
1373
 
1374
+ const decodeEntities = (text) => {
1375
+ const el = document.createElement('span');
1376
+ el.innerHTML = text;
1377
+ return el.textContent;
1378
+ };
1379
+
1365
1380
  while (index < input.length) {
1366
1381
  const tagStart = input.indexOf('<', index);
1367
1382
 
1368
1383
  if (tagStart === -1) {
1369
- currentParent().appendChild(document.createTextNode(input.slice(index)));
1384
+ currentParent().appendChild(document.createTextNode(decodeEntities(input.slice(index))));
1370
1385
  break;
1371
1386
  }
1372
1387
 
1373
1388
  if (tagStart > index) {
1374
- currentParent().appendChild(document.createTextNode(input.slice(index, tagStart)));
1389
+ currentParent().appendChild(document.createTextNode(decodeEntities(input.slice(index, tagStart))));
1375
1390
  }
1376
1391
 
1377
1392
  if (input.slice(tagStart, tagStart + 4) === '<!--') {
@@ -1719,6 +1734,7 @@
1719
1734
  table: '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z"/></svg>',
1720
1735
  quote: '<svg viewBox="0 0 24 24"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>',
1721
1736
  code: '<svg viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>',
1737
+ 'code-inline': '<svg viewBox="0 0 256 256"><path d="M0 0h256v256H0z" fill="none"/><path fill="currentColor" d="M71.68 97.22L34.74 128l36.94 30.78a12 12 0 1 1-15.36 18.44l-48-40a12 12 0 0 1 0-18.44l48-40a12 12 0 0 1 15.36 18.44m176 21.56l-48-40a12 12 0 1 0-15.36 18.44L221.26 128l-36.94 30.78a12 12 0 1 0 15.36 18.44l48-40a12 12 0 0 0 0-18.44M164.1 28.72a12 12 0 0 0-15.38 7.18l-64 176a12 12 0 0 0 7.18 15.37a11.8 11.8 0 0 0 4.1.73a12 12 0 0 0 11.28-7.9l64-176a12 12 0 0 0-7.18-15.38"/></svg>',
1722
1738
  minus: '<svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>',
1723
1739
  eraser: '<svg viewBox="0 0 24 24"><path d="M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 01-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zm-1.41 1.42L6.93 12.9l4.24 4.24 7.9-7.9-4.24-4.26z"/></svg>',
1724
1740
  fullscreen: '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>',
@@ -2512,7 +2528,7 @@
2512
2528
  <img src="https://github.com/neikiri/neiki-editor/raw/main/logo.png" alt="Neiki's Editor" style="width: 120px; height: auto; margin: 0 auto 16px; display: block;">
2513
2529
  <div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
2514
2530
  <div><strong>${Utils.escapeHTML(t('help.author'))}:</strong> neikiri (Jindřich Stoklasa)</div>
2515
- <div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 2.9.5</div>
2531
+ <div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 2.10.0</div>
2516
2532
  <div><strong>${Utils.escapeHTML(t('help.github'))}:</strong> <a href="https://github.com/neikiri/neiki-editor" target="_blank" rel="noopener noreferrer" style="color: var(--neiki-accent);">github.com/neikiri/neiki-editor</a></div>
2517
2533
  <div><strong>${Utils.escapeHTML(t('help.documentation'))}:</strong> <a href="https://github.com/neikiri/neiki-editor/wiki" target="_blank" rel="noopener noreferrer" style="color: var(--neiki-accent);">Wiki</a></div>
2518
2534
  </div>
@@ -2971,6 +2987,112 @@
2971
2987
  subscript() { this.exec('subscript'); }
2972
2988
  superscript() { this.exec('superscript'); }
2973
2989
 
2990
+ toggleCode() {
2991
+ const sel = window.getSelection();
2992
+ if (!sel || !sel.rangeCount) return;
2993
+
2994
+ const range = sel.getRangeAt(0);
2995
+
2996
+ // Detect if selection is inside or contains <pre> or <code>
2997
+ const findCodeAncestor = (n) => {
2998
+ if (n.nodeType === Node.TEXT_NODE) n = n.parentNode;
2999
+ if (!n || !n.closest) return null;
3000
+ const pre = n.closest('pre');
3001
+ if (pre && this.editor.contentArea.contains(pre)) return { type: 'pre', el: pre };
3002
+ const code = n.closest('code');
3003
+ if (code && this.editor.contentArea.contains(code)) return { type: 'code', el: code };
3004
+ return null;
3005
+ };
3006
+
3007
+ // Check start, end, and common ancestor
3008
+ const fromStart = findCodeAncestor(range.startContainer);
3009
+ const fromEnd = findCodeAncestor(range.endContainer);
3010
+ const fromCommon = findCodeAncestor(range.commonAncestorContainer);
3011
+ const existing = fromStart || fromEnd || fromCommon;
3012
+
3013
+ // Also check if selection contains <code> elements inside it
3014
+ let containedCode = null;
3015
+ if (!existing && !range.collapsed) {
3016
+ const container = range.commonAncestorContainer;
3017
+ const el = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
3018
+ if (el) containedCode = el.querySelector ? el.querySelector('code') : null;
3019
+ }
3020
+
3021
+ if (existing && existing.type === 'pre') {
3022
+ // Unwrap <pre><code> block: convert back to <p> paragraphs
3023
+ const pre = existing.el;
3024
+ const lines = pre.textContent.split('\n');
3025
+ const fragment = document.createDocumentFragment();
3026
+ lines.forEach(line => {
3027
+ const p = document.createElement('p');
3028
+ p.textContent = line || '\u00A0';
3029
+ fragment.appendChild(p);
3030
+ });
3031
+ pre.parentNode.replaceChild(fragment, pre);
3032
+ } else if (existing && existing.type === 'code') {
3033
+ // Unwrap inline <code>
3034
+ const codeEl = existing.el;
3035
+ const parent = codeEl.parentNode;
3036
+ while (codeEl.firstChild) {
3037
+ parent.insertBefore(codeEl.firstChild, codeEl);
3038
+ }
3039
+ parent.removeChild(codeEl);
3040
+ } else if (containedCode) {
3041
+ // Selection contains <code> elements — unwrap them all
3042
+ const container = range.commonAncestorContainer;
3043
+ const el = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
3044
+ const codes = el.querySelectorAll('code');
3045
+ codes.forEach(codeEl => {
3046
+ if (this.editor.contentArea.contains(codeEl)) {
3047
+ const p = codeEl.parentNode;
3048
+ while (codeEl.firstChild) p.insertBefore(codeEl.firstChild, codeEl);
3049
+ p.removeChild(codeEl);
3050
+ }
3051
+ });
3052
+ } else {
3053
+ // Apply code formatting
3054
+ if (sel.isCollapsed) this._expandToWordIfCollapsed();
3055
+
3056
+ const startBlock = this._getBlockParent(range.startContainer);
3057
+ const endBlock = this._getBlockParent(range.endContainer);
3058
+
3059
+ if (startBlock && endBlock && startBlock !== endBlock) {
3060
+ // Multi-block selection: create a <pre><code> block
3061
+ const blocks = [];
3062
+ let cur = startBlock;
3063
+ while (cur) {
3064
+ blocks.push(cur);
3065
+ if (cur === endBlock) break;
3066
+ cur = cur.nextElementSibling;
3067
+ }
3068
+
3069
+ const textLines = blocks.map(block => block.textContent);
3070
+ const pre = document.createElement('pre');
3071
+ const code = document.createElement('code');
3072
+ code.textContent = textLines.join('\n');
3073
+ pre.appendChild(code);
3074
+
3075
+ startBlock.parentNode.insertBefore(pre, startBlock);
3076
+ blocks.forEach(block => block.parentNode.removeChild(block));
3077
+ } else {
3078
+ // Single block: inline <code> wrap
3079
+ this.editor.wrapSelection('code');
3080
+ }
3081
+ }
3082
+ this.editor.history.record();
3083
+ this.editor.updateToolbar();
3084
+ this.editor.triggerChange();
3085
+ }
3086
+
3087
+ _getBlockParent(node) {
3088
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
3089
+ while (node && node !== this.editor.contentArea) {
3090
+ if (node.parentNode === this.editor.contentArea) return node;
3091
+ node = node.parentNode;
3092
+ }
3093
+ return null;
3094
+ }
3095
+
2974
3096
  justifyLeft() { this.exec('justifyLeft'); }
2975
3097
  justifyCenter() { this.exec('justifyCenter'); }
2976
3098
  justifyRight() { this.exec('justifyRight'); }
@@ -3228,6 +3350,11 @@
3228
3350
  'neiki_' + (typeof element === 'string' ? element.replace(/[^a-zA-Z0-9]/g, '_') : 'editor');
3229
3351
 
3230
3352
  this.config = Utils.deepMerge(DEFAULT_CONFIG, options);
3353
+
3354
+ // Backward compatibility: support old custom_class option
3355
+ if (this.config.custom_class && !this.config.customClass) {
3356
+ this.config.customClass = this.config.custom_class;
3357
+ }
3231
3358
  this.instanceIndex = ++EDITOR_INSTANCE_COUNTER;
3232
3359
  this.storageId = this.createAutosaveStorageId(element);
3233
3360
  this.isFullscreen = false;
@@ -3815,8 +3942,8 @@
3815
3942
  createContentArea() {
3816
3943
  this.contentWrapper = Utils.createElement('div', { className: 'neiki-content-wrapper' });
3817
3944
 
3818
- const contentClasses = this.config.custom_class
3819
- ? `neiki-content ${this.config.custom_class}`
3945
+ const contentClasses = this.config.customClass
3946
+ ? `neiki-content ${this.config.customClass}`
3820
3947
  : 'neiki-content';
3821
3948
 
3822
3949
  this.contentArea = Utils.createElement('div', {
@@ -4179,6 +4306,17 @@
4179
4306
  case 'justifyFull':
4180
4307
  isActive = document.queryCommandState(config.command);
4181
4308
  break;
4309
+ case 'toggleCode': {
4310
+ const sel = window.getSelection();
4311
+ if (sel && sel.rangeCount) {
4312
+ let node = sel.getRangeAt(0).startContainer;
4313
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
4314
+ const inCode = node.closest && node.closest('code') && this.contentArea.contains(node.closest('code'));
4315
+ const inPre = node.closest && node.closest('pre') && this.contentArea.contains(node.closest('pre'));
4316
+ isActive = !!(inCode || inPre);
4317
+ }
4318
+ break;
4319
+ }
4182
4320
  case 'formatBlock':
4183
4321
  if (config.value === 'blockquote') {
4184
4322
  const sel = window.getSelection();
@@ -4372,15 +4510,48 @@
4372
4510
  } catch (e) {}
4373
4511
  return;
4374
4512
  }
4375
- // Wrap bare text nodes in <p>
4513
+ // Wrap bare text nodes and normalize non-block elements into <p>
4514
+ const BLOCK_TAGS = new Set(['P','H1','H2','H3','H4','H5','H6','UL','OL','BLOCKQUOTE','TABLE','HR','PRE','FIGURE','SECTION','ARTICLE']);
4376
4515
  const childNodes = Array.from(this.contentArea.childNodes);
4516
+
4517
+ // First pass: convert stray <div> to <p>
4377
4518
  childNodes.forEach(node => {
4378
- if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
4519
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'DIV') {
4379
4520
  const p = document.createElement('p');
4380
- node.parentNode.insertBefore(p, node);
4381
- p.appendChild(node);
4521
+ while (node.firstChild) p.appendChild(node.firstChild);
4522
+ node.parentNode.replaceChild(p, node);
4382
4523
  }
4383
4524
  });
4525
+
4526
+ // Second pass: group consecutive inline/text nodes into <p>
4527
+ const updatedNodes = Array.from(this.contentArea.childNodes);
4528
+ let inlineGroup = [];
4529
+
4530
+ const flushGroup = () => {
4531
+ if (inlineGroup.length === 0) return;
4532
+ const p = document.createElement('p');
4533
+ const firstNode = inlineGroup[0];
4534
+ this.contentArea.insertBefore(p, firstNode);
4535
+ inlineGroup.forEach(n => p.appendChild(n));
4536
+ inlineGroup = [];
4537
+ };
4538
+
4539
+ updatedNodes.forEach(node => {
4540
+ if (node.nodeType === Node.TEXT_NODE) {
4541
+ if (node.textContent.trim()) {
4542
+ inlineGroup.push(node);
4543
+ } else if (inlineGroup.length > 0) {
4544
+ inlineGroup.push(node);
4545
+ }
4546
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
4547
+ if (BLOCK_TAGS.has(node.tagName)) {
4548
+ flushGroup();
4549
+ } else {
4550
+ inlineGroup.push(node);
4551
+ }
4552
+ }
4553
+ });
4554
+ flushGroup();
4384
4555
  }
4385
4556
 
4386
4557
  syncToOriginal() {