neiki-editor 2.9.5 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.1-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.1/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.1/neiki-editor.css">
77
+ <script src="https://cdn.neikiri.dev/neiki-editor/2.10.1/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.1/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.1/dist/neiki-editor.css">
95
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.10.1/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.1
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.1
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,23 @@
1362
1371
 
1363
1372
  const currentParent = () => stack[stack.length - 1];
1364
1373
 
1374
+ const ENTITY_MAP = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: '\u00A0' };
1375
+ const decodeEntities = (text) => text.replace(/&(?:#x([0-9a-fA-F]+)|#([0-9]+)|([a-zA-Z]+));/g, (m, hex, dec, named) => {
1376
+ if (hex) return String.fromCodePoint(parseInt(hex, 16));
1377
+ if (dec) return String.fromCodePoint(parseInt(dec, 10));
1378
+ return ENTITY_MAP[named] || m;
1379
+ });
1380
+
1365
1381
  while (index < input.length) {
1366
1382
  const tagStart = input.indexOf('<', index);
1367
1383
 
1368
1384
  if (tagStart === -1) {
1369
- currentParent().appendChild(document.createTextNode(input.slice(index)));
1385
+ currentParent().appendChild(document.createTextNode(decodeEntities(input.slice(index))));
1370
1386
  break;
1371
1387
  }
1372
1388
 
1373
1389
  if (tagStart > index) {
1374
- currentParent().appendChild(document.createTextNode(input.slice(index, tagStart)));
1390
+ currentParent().appendChild(document.createTextNode(decodeEntities(input.slice(index, tagStart))));
1375
1391
  }
1376
1392
 
1377
1393
  if (input.slice(tagStart, tagStart + 4) === '<!--') {
@@ -1719,6 +1735,7 @@
1719
1735
  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
1736
  quote: '<svg viewBox="0 0 24 24"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>',
1721
1737
  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>',
1738
+ '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
1739
  minus: '<svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>',
1723
1740
  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
1741
  fullscreen: '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>',
@@ -2512,7 +2529,7 @@
2512
2529
  <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
2530
  <div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
2514
2531
  <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>
2532
+ <div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 2.10.1</div>
2516
2533
  <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
2534
  <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
2535
  </div>
@@ -2971,6 +2988,112 @@
2971
2988
  subscript() { this.exec('subscript'); }
2972
2989
  superscript() { this.exec('superscript'); }
2973
2990
 
2991
+ toggleCode() {
2992
+ const sel = window.getSelection();
2993
+ if (!sel || !sel.rangeCount) return;
2994
+
2995
+ const range = sel.getRangeAt(0);
2996
+
2997
+ // Detect if selection is inside or contains <pre> or <code>
2998
+ const findCodeAncestor = (n) => {
2999
+ if (n.nodeType === Node.TEXT_NODE) n = n.parentNode;
3000
+ if (!n || !n.closest) return null;
3001
+ const pre = n.closest('pre');
3002
+ if (pre && this.editor.contentArea.contains(pre)) return { type: 'pre', el: pre };
3003
+ const code = n.closest('code');
3004
+ if (code && this.editor.contentArea.contains(code)) return { type: 'code', el: code };
3005
+ return null;
3006
+ };
3007
+
3008
+ // Check start, end, and common ancestor
3009
+ const fromStart = findCodeAncestor(range.startContainer);
3010
+ const fromEnd = findCodeAncestor(range.endContainer);
3011
+ const fromCommon = findCodeAncestor(range.commonAncestorContainer);
3012
+ const existing = fromStart || fromEnd || fromCommon;
3013
+
3014
+ // Also check if selection contains <code> elements inside it
3015
+ let containedCode = null;
3016
+ if (!existing && !range.collapsed) {
3017
+ const container = range.commonAncestorContainer;
3018
+ const el = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
3019
+ if (el) containedCode = el.querySelector ? el.querySelector('code') : null;
3020
+ }
3021
+
3022
+ if (existing && existing.type === 'pre') {
3023
+ // Unwrap <pre><code> block: convert back to <p> paragraphs
3024
+ const pre = existing.el;
3025
+ const lines = pre.textContent.split('\n');
3026
+ const fragment = document.createDocumentFragment();
3027
+ lines.forEach(line => {
3028
+ const p = document.createElement('p');
3029
+ p.textContent = line || '\u00A0';
3030
+ fragment.appendChild(p);
3031
+ });
3032
+ pre.parentNode.replaceChild(fragment, pre);
3033
+ } else if (existing && existing.type === 'code') {
3034
+ // Unwrap inline <code>
3035
+ const codeEl = existing.el;
3036
+ const parent = codeEl.parentNode;
3037
+ while (codeEl.firstChild) {
3038
+ parent.insertBefore(codeEl.firstChild, codeEl);
3039
+ }
3040
+ parent.removeChild(codeEl);
3041
+ } else if (containedCode) {
3042
+ // Selection contains <code> elements — unwrap them all
3043
+ const container = range.commonAncestorContainer;
3044
+ const el = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
3045
+ const codes = el.querySelectorAll('code');
3046
+ codes.forEach(codeEl => {
3047
+ if (this.editor.contentArea.contains(codeEl)) {
3048
+ const p = codeEl.parentNode;
3049
+ while (codeEl.firstChild) p.insertBefore(codeEl.firstChild, codeEl);
3050
+ p.removeChild(codeEl);
3051
+ }
3052
+ });
3053
+ } else {
3054
+ // Apply code formatting
3055
+ if (sel.isCollapsed) this._expandToWordIfCollapsed();
3056
+
3057
+ const startBlock = this._getBlockParent(range.startContainer);
3058
+ const endBlock = this._getBlockParent(range.endContainer);
3059
+
3060
+ if (startBlock && endBlock && startBlock !== endBlock) {
3061
+ // Multi-block selection: create a <pre><code> block
3062
+ const blocks = [];
3063
+ let cur = startBlock;
3064
+ while (cur) {
3065
+ blocks.push(cur);
3066
+ if (cur === endBlock) break;
3067
+ cur = cur.nextElementSibling;
3068
+ }
3069
+
3070
+ const textLines = blocks.map(block => block.textContent);
3071
+ const pre = document.createElement('pre');
3072
+ const code = document.createElement('code');
3073
+ code.textContent = textLines.join('\n');
3074
+ pre.appendChild(code);
3075
+
3076
+ startBlock.parentNode.insertBefore(pre, startBlock);
3077
+ blocks.forEach(block => block.parentNode.removeChild(block));
3078
+ } else {
3079
+ // Single block: inline <code> wrap
3080
+ this.editor.wrapSelection('code');
3081
+ }
3082
+ }
3083
+ this.editor.history.record();
3084
+ this.editor.updateToolbar();
3085
+ this.editor.triggerChange();
3086
+ }
3087
+
3088
+ _getBlockParent(node) {
3089
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
3090
+ while (node && node !== this.editor.contentArea) {
3091
+ if (node.parentNode === this.editor.contentArea) return node;
3092
+ node = node.parentNode;
3093
+ }
3094
+ return null;
3095
+ }
3096
+
2974
3097
  justifyLeft() { this.exec('justifyLeft'); }
2975
3098
  justifyCenter() { this.exec('justifyCenter'); }
2976
3099
  justifyRight() { this.exec('justifyRight'); }
@@ -3228,6 +3351,11 @@
3228
3351
  'neiki_' + (typeof element === 'string' ? element.replace(/[^a-zA-Z0-9]/g, '_') : 'editor');
3229
3352
 
3230
3353
  this.config = Utils.deepMerge(DEFAULT_CONFIG, options);
3354
+
3355
+ // Backward compatibility: support old custom_class option
3356
+ if (this.config.custom_class && !this.config.customClass) {
3357
+ this.config.customClass = this.config.custom_class;
3358
+ }
3231
3359
  this.instanceIndex = ++EDITOR_INSTANCE_COUNTER;
3232
3360
  this.storageId = this.createAutosaveStorageId(element);
3233
3361
  this.isFullscreen = false;
@@ -3815,8 +3943,8 @@
3815
3943
  createContentArea() {
3816
3944
  this.contentWrapper = Utils.createElement('div', { className: 'neiki-content-wrapper' });
3817
3945
 
3818
- const contentClasses = this.config.custom_class
3819
- ? `neiki-content ${this.config.custom_class}`
3946
+ const contentClasses = this.config.customClass
3947
+ ? `neiki-content ${this.config.customClass}`
3820
3948
  : 'neiki-content';
3821
3949
 
3822
3950
  this.contentArea = Utils.createElement('div', {
@@ -4179,6 +4307,17 @@
4179
4307
  case 'justifyFull':
4180
4308
  isActive = document.queryCommandState(config.command);
4181
4309
  break;
4310
+ case 'toggleCode': {
4311
+ const sel = window.getSelection();
4312
+ if (sel && sel.rangeCount) {
4313
+ let node = sel.getRangeAt(0).startContainer;
4314
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
4315
+ const inCode = node.closest && node.closest('code') && this.contentArea.contains(node.closest('code'));
4316
+ const inPre = node.closest && node.closest('pre') && this.contentArea.contains(node.closest('pre'));
4317
+ isActive = !!(inCode || inPre);
4318
+ }
4319
+ break;
4320
+ }
4182
4321
  case 'formatBlock':
4183
4322
  if (config.value === 'blockquote') {
4184
4323
  const sel = window.getSelection();
@@ -4372,15 +4511,48 @@
4372
4511
  } catch (e) {}
4373
4512
  return;
4374
4513
  }
4375
- // Wrap bare text nodes in <p>
4514
+ // Wrap bare text nodes and normalize non-block elements into <p>
4515
+ const BLOCK_TAGS = new Set(['P','H1','H2','H3','H4','H5','H6','UL','OL','BLOCKQUOTE','TABLE','HR','PRE','FIGURE','SECTION','ARTICLE']);
4376
4516
  const childNodes = Array.from(this.contentArea.childNodes);
4517
+
4518
+ // First pass: convert stray <div> to <p>
4377
4519
  childNodes.forEach(node => {
4378
- if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
4520
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'DIV') {
4379
4521
  const p = document.createElement('p');
4380
- node.parentNode.insertBefore(p, node);
4381
- p.appendChild(node);
4522
+ while (node.firstChild) p.appendChild(node.firstChild);
4523
+ node.parentNode.replaceChild(p, node);
4382
4524
  }
4383
4525
  });
4526
+
4527
+ // Second pass: group consecutive inline/text nodes into <p>
4528
+ const updatedNodes = Array.from(this.contentArea.childNodes);
4529
+ let inlineGroup = [];
4530
+
4531
+ const flushGroup = () => {
4532
+ if (inlineGroup.length === 0) return;
4533
+ const p = document.createElement('p');
4534
+ const firstNode = inlineGroup[0];
4535
+ this.contentArea.insertBefore(p, firstNode);
4536
+ inlineGroup.forEach(n => p.appendChild(n));
4537
+ inlineGroup = [];
4538
+ };
4539
+
4540
+ updatedNodes.forEach(node => {
4541
+ if (node.nodeType === Node.TEXT_NODE) {
4542
+ if (node.textContent.trim()) {
4543
+ inlineGroup.push(node);
4544
+ } else if (inlineGroup.length > 0) {
4545
+ inlineGroup.push(node);
4546
+ }
4547
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
4548
+ if (BLOCK_TAGS.has(node.tagName)) {
4549
+ flushGroup();
4550
+ } else {
4551
+ inlineGroup.push(node);
4552
+ }
4553
+ }
4554
+ });
4555
+ flushGroup();
4384
4556
  }
4385
4557
 
4386
4558
  syncToOriginal() {