@limetech/lime-elements 39.39.1 → 39.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/index.cjs.js +2 -1
  3. package/dist/cjs/lime-elements.cjs.js +1 -1
  4. package/dist/cjs/limel-file.cjs.entry.js +15 -5
  5. package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +440 -212
  6. package/dist/cjs/limel-text-editor.cjs.entry.js +2 -2
  7. package/dist/cjs/loader.cjs.js +1 -1
  8. package/dist/cjs/{types-Bu6wcdI0.js → types-C7gNcwzE.js} +12 -0
  9. package/dist/collection/components/file/file.js +16 -5
  10. package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/inserter.js +101 -30
  11. package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/node.js +160 -54
  12. package/dist/collection/components/text-editor/prosemirror-adapter/plugins/image/view.js +6 -1
  13. package/dist/collection/components/text-editor/prosemirror-adapter/prosemirror-adapter.js +51 -5
  14. package/dist/collection/components/text-editor/text-editor.js +33 -3
  15. package/dist/collection/components/text-editor/text-editor.types.js +10 -1
  16. package/dist/collection/components/text-editor/utils/content-type-converter.js +30 -1
  17. package/dist/collection/components/text-editor/utils/html-converter.js +4 -2
  18. package/dist/collection/components/text-editor/utils/markdown-converter.js +9 -5
  19. package/dist/esm/index.js +1 -1
  20. package/dist/esm/lime-elements.js +1 -1
  21. package/dist/esm/limel-file.entry.js +15 -5
  22. package/dist/esm/limel-prosemirror-adapter.entry.js +440 -212
  23. package/dist/esm/limel-text-editor.entry.js +2 -2
  24. package/dist/esm/loader.js +1 -1
  25. package/dist/esm/{types-BWYo6dLf.js → types-C_Yu8dOg.js} +12 -1
  26. package/dist/lime-elements/index.esm.js +1 -1
  27. package/dist/lime-elements/lime-elements.esm.js +1 -1
  28. package/dist/lime-elements/p-1f35fc44.entry.js +1 -0
  29. package/dist/lime-elements/p-56781dc7.entry.js +1 -0
  30. package/dist/lime-elements/p-5cf7464f.entry.js +1 -0
  31. package/dist/lime-elements/p-C_Yu8dOg.js +1 -0
  32. package/dist/types/components/file/file.d.ts +3 -0
  33. package/dist/types/components/text-editor/prosemirror-adapter/plugins/image/inserter.d.ts +2 -2
  34. package/dist/types/components/text-editor/prosemirror-adapter/plugins/image/node.d.ts +41 -19
  35. package/dist/types/components/text-editor/prosemirror-adapter/prosemirror-adapter.d.ts +13 -1
  36. package/dist/types/components/text-editor/text-editor.d.ts +10 -2
  37. package/dist/types/components/text-editor/text-editor.types.d.ts +52 -0
  38. package/dist/types/components/text-editor/utils/content-type-converter.d.ts +12 -0
  39. package/dist/types/components/text-editor/utils/html-converter.d.ts +3 -1
  40. package/dist/types/components/text-editor/utils/markdown-converter.d.ts +3 -1
  41. package/dist/types/components.d.ts +34 -6
  42. package/dist/types/global/shared-types/file.types.d.ts +10 -0
  43. package/package.json +1 -1
  44. package/dist/lime-elements/p-366419ee.entry.js +0 -1
  45. package/dist/lime-elements/p-BWYo6dLf.js +0 -1
  46. package/dist/lime-elements/p-b7d734d2.entry.js +0 -1
  47. package/dist/lime-elements/p-b97f2f56.entry.js +0 -1
@@ -1,21 +1,39 @@
1
+ import { isInlineImageTag, } from "../../../text-editor.types";
1
2
  import translate from "../../../../../global/translations";
2
3
  export const imageCache = new Map();
4
+ export const IMAGE_ID_ATTRIBUTE = 'image-id';
3
5
  /**
4
- *
6
+ * The attribute names accepted for an inline-image tag. Backs the sanitizer
7
+ * whitelist built in `buildContentWhitelist`, so a stored tag keeps these
8
+ * attributes through parsing. The serializers here enumerate their own output
9
+ * attributes separately and are not driven by this list.
10
+ */
11
+ export const INLINE_IMAGE_ATTRIBUTES = [
12
+ IMAGE_ID_ATTRIBUTE,
13
+ 'width',
14
+ 'height',
15
+ 'alt',
16
+ ];
17
+ /**
18
+ * Builds the `image` NodeSpec, wired for inline-image parsing/serialization
19
+ * when `inlineImages` is configured.
5
20
  * @param language
21
+ * @param inlineImages
6
22
  */
7
- export function getImageNode(language) {
8
- return { image: createImageNodeSpec(language) };
23
+ export function getImageNode(language, inlineImages) {
24
+ return { image: createImageNodeSpec(language, inlineImages) };
9
25
  }
10
26
  /**
11
- *
27
+ * Builds the markdown serializer for the `image` node, emitting the inline-image
28
+ * tag when `inlineImages` is configured and a `<img>` otherwise.
12
29
  * @param language
30
+ * @param inlineImages
13
31
  */
14
- export function getImageNodeMarkdownSerializer(language) {
15
- return { image: createImageNodeMarkdownSerializer(language) };
32
+ export function getImageNodeMarkdownSerializer(language, inlineImages) {
33
+ return { image: createImageNodeMarkdownSerializer(language, inlineImages) };
16
34
  }
17
35
  /**
18
- *
36
+ * Applies the node's stored dimensions to a rendered `<img>` element.
19
37
  * @param img
20
38
  * @param node
21
39
  */
@@ -27,8 +45,8 @@ export function applyImageStyles(img, node) {
27
45
  img.style.maxWidth = node.attrs.maxWidth;
28
46
  }
29
47
  /**
30
- * Recursively checks if a ProseMirror node or
31
- * any of its child nodes is an image node.
48
+ * Recursively checks if a ProseMirror node or any of its descendants is an
49
+ * image node.
32
50
  * @param node
33
51
  */
34
52
  export function hasImageNode(node) {
@@ -43,14 +61,20 @@ export function hasImageNode(node) {
43
61
  }
44
62
  return false;
45
63
  }
46
- function createImageNodeMarkdownSerializer(language) {
64
+ function createImageNodeMarkdownSerializer(language, inlineImages) {
47
65
  return (markdownSerializerState, node) => {
48
66
  const state = node.attrs.state;
49
67
  if (!isEditorImageState(state)) {
50
68
  return;
51
69
  }
52
70
  if (state === 'success') {
53
- const imageHTML = getImageHTML(node.attrs);
71
+ const attrs = node.attrs;
72
+ const tag = inlineImages && isInlineImageTag(inlineImages)
73
+ ? inlineImages
74
+ : undefined;
75
+ const imageHTML = tag && attrs.imageId
76
+ ? getInlineImageHTML(attrs, tag.tagName)
77
+ : getImageHTML(attrs);
54
78
  markdownSerializerState.write(imageHTML);
55
79
  return;
56
80
  }
@@ -58,6 +82,44 @@ function createImageNodeMarkdownSerializer(language) {
58
82
  markdownSerializerState.write(statusHTML);
59
83
  };
60
84
  }
85
+ function escapeAttributeValue(value) {
86
+ return value
87
+ .replaceAll('&', '&amp;')
88
+ .replaceAll('"', '&quot;')
89
+ .replaceAll('<', '&lt;')
90
+ .replaceAll('>', '&gt;');
91
+ }
92
+ // Dimensions are emitted as CSS-unit strings (e.g. `width="300px"`), matching
93
+ // what `applyImageStyles` reads back when the tag is re-parsed inside the
94
+ // editor. The external renderer of this microformat (the `limebb-inline-image`
95
+ // building block) must therefore treat `width`/`height` as CSS lengths, not as
96
+ // bare HTML pixel counts.
97
+ function getInlineImageHTML(attrs, tag) {
98
+ var _a;
99
+ const attributes = [
100
+ `${IMAGE_ID_ATTRIBUTE}="${escapeAttributeValue((_a = attrs.imageId) !== null && _a !== void 0 ? _a : '')}"`,
101
+ attrs.width ? `width="${escapeAttributeValue(attrs.width)}"` : '',
102
+ attrs.height ? `height="${escapeAttributeValue(attrs.height)}"` : '',
103
+ attrs.alt ? `alt="${escapeAttributeValue(attrs.alt)}"` : '',
104
+ ].filter(Boolean);
105
+ return `<${tag} ${attributes.join(' ')}></${tag}>`;
106
+ }
107
+ function buildInlineImageDOMAttrs(attrs) {
108
+ var _a;
109
+ const result = {
110
+ [IMAGE_ID_ATTRIBUTE]: (_a = attrs.imageId) !== null && _a !== void 0 ? _a : '',
111
+ };
112
+ if (attrs.alt) {
113
+ result.alt = attrs.alt;
114
+ }
115
+ if (attrs.width) {
116
+ result.width = attrs.width;
117
+ }
118
+ if (attrs.height) {
119
+ result.height = attrs.height;
120
+ }
121
+ return result;
122
+ }
61
123
  function getStatusHTML(state, alt, language) {
62
124
  const key = state === 'failed' ? 'failed' : 'loading';
63
125
  const text = translate.get(`editor-image-view.${key}`, language, {
@@ -66,26 +128,23 @@ function getStatusHTML(state, alt, language) {
66
128
  return `<span>${text}</span>`;
67
129
  }
68
130
  function getImageHTML(attrs) {
69
- const style = [];
70
- if (attrs.height) {
71
- style.push(`height: ${attrs.height};`);
72
- }
73
- if (attrs.width) {
74
- style.push(`width: ${attrs.width};`);
75
- }
76
- if (attrs.minHeight) {
77
- style.push(`min-height: ${attrs.minHeight};`);
78
- }
79
- if (attrs.minWidth) {
80
- style.push(`min-width: ${attrs.minWidth};`);
81
- }
82
- if (attrs.maxWidth) {
83
- style.push(`max-width: ${attrs.maxWidth};`);
84
- }
85
- const styleAttribute = style.length > 0 ? ` style="${style.join('')}"` : '';
86
- return `<img src="${attrs.src}" alt="${attrs.alt}"${styleAttribute} />`;
131
+ const dimensions = [
132
+ ['height', attrs.height],
133
+ ['width', attrs.width],
134
+ ['min-height', attrs.minHeight],
135
+ ['min-width', attrs.minWidth],
136
+ ['max-width', attrs.maxWidth],
137
+ ];
138
+ const style = dimensions
139
+ .filter(([, value]) => value)
140
+ .map(([property, value]) => `${property}: ${value};`)
141
+ .join('');
142
+ const styleAttribute = style
143
+ ? ` style="${escapeAttributeValue(style)}"`
144
+ : '';
145
+ return `<img src="${escapeAttributeValue(attrs.src)}" alt="${escapeAttributeValue(attrs.alt)}"${styleAttribute} />`;
87
146
  }
88
- function createImageNodeSpec(language) {
147
+ function createImageNodeSpec(language, inlineImages) {
89
148
  return {
90
149
  group: 'inline',
91
150
  inline: true,
@@ -93,6 +152,7 @@ function createImageNodeSpec(language) {
93
152
  src: { default: '' },
94
153
  alt: { default: '' },
95
154
  fileInfoId: { default: '' },
155
+ imageId: { default: '' },
96
156
  height: { default: '' },
97
157
  width: { default: '' },
98
158
  minHeight: { default: '' },
@@ -100,33 +160,79 @@ function createImageNodeSpec(language) {
100
160
  maxWidth: { default: '100%' },
101
161
  state: { default: 'success' },
102
162
  },
103
- toDOM: (node) => {
104
- if (!isEditorImageState(node.attrs.state)) {
105
- return;
106
- }
107
- if (node.attrs.state === 'success') {
108
- return getOrCreateImageElement(node.attrs.fileInfoId, node);
109
- }
163
+ toDOM: createImageToDOM(language, inlineImages),
164
+ parseDOM: createImageParseDOM(inlineImages),
165
+ };
166
+ }
167
+ function createImageToDOM(language, inlineImages) {
168
+ return (node) => {
169
+ if (!isEditorImageState(node.attrs.state)) {
170
+ return;
171
+ }
172
+ if (node.attrs.state !== 'success') {
110
173
  return createStatusSpanForState(node.attrs.state, node, language);
111
- },
112
- parseDOM: [
113
- {
114
- tag: 'img',
115
- getAttrs: (dom) => {
116
- return {
117
- src: dom.getAttribute('src') || '',
118
- alt: dom.getAttribute('alt') || 'file',
119
- width: dom.style.width || '',
120
- height: dom.style.height || '',
121
- maxWidth: '100%',
122
- state: 'success',
123
- fileInfoId: crypto.randomUUID(),
124
- };
125
- },
126
- },
127
- ],
174
+ }
175
+ // Emit the inline-image tag for HTML serialization and clipboard;
176
+ // on-screen rendering comes from the NodeView, not this.
177
+ const tag = inlineImages && isInlineImageTag(inlineImages)
178
+ ? inlineImages
179
+ : undefined;
180
+ if (tag && node.attrs.imageId) {
181
+ return [
182
+ tag.tagName,
183
+ buildInlineImageDOMAttrs(node.attrs),
184
+ ];
185
+ }
186
+ return getOrCreateImageElement(node.attrs.fileInfoId, node);
128
187
  };
129
188
  }
189
+ function createImageParseDOM(inlineImages) {
190
+ const rules = [];
191
+ const tag = inlineImages && isInlineImageTag(inlineImages)
192
+ ? inlineImages
193
+ : undefined;
194
+ if (tag) {
195
+ rules.push({
196
+ tag: tag.tagName,
197
+ getAttrs: (dom) => {
198
+ const imageId = dom.getAttribute(IMAGE_ID_ATTRIBUTE);
199
+ // Skip malformed tags without an id; parsing them would
200
+ // produce a broken image from an empty getUrl('').
201
+ if (!imageId) {
202
+ return false;
203
+ }
204
+ return {
205
+ src: tag.getUrl(imageId),
206
+ // Preserve an absent alt as empty so an id-only tag
207
+ // serializes back to the same markup instead of gaining
208
+ // a synthetic alt, keeping the round-trip stable.
209
+ alt: dom.getAttribute('alt') || '',
210
+ imageId: imageId,
211
+ width: dom.getAttribute('width') || '',
212
+ height: dom.getAttribute('height') || '',
213
+ maxWidth: '100%',
214
+ state: 'success',
215
+ fileInfoId: crypto.randomUUID(),
216
+ };
217
+ },
218
+ });
219
+ }
220
+ rules.push({
221
+ tag: 'img',
222
+ getAttrs: (dom) => {
223
+ return {
224
+ src: dom.getAttribute('src') || '',
225
+ alt: dom.getAttribute('alt') || 'file',
226
+ width: dom.style.width || '',
227
+ height: dom.style.height || '',
228
+ maxWidth: '100%',
229
+ state: 'success',
230
+ fileInfoId: crypto.randomUUID(),
231
+ };
232
+ },
233
+ });
234
+ return rules;
235
+ }
130
236
  function isEditorImageState(state) {
131
237
  return state === 'loading' || state === 'failed' || state === 'success';
132
238
  }
@@ -119,7 +119,12 @@ class ImageView {
119
119
  this.img.alt = node.attrs.alt;
120
120
  applyImageStyles(this.img, node);
121
121
  this.img.addEventListener('load', () => {
122
- this.persistDimensions();
122
+ // Only measure a freshly inserted image. Re-measuring one that
123
+ // already has a width would rewrite it against the current
124
+ // container size and emit a spurious change event on every load.
125
+ if (!this.node.attrs.width) {
126
+ this.persistDimensions();
127
+ }
123
128
  });
124
129
  this.dom.append(this.img);
125
130
  this.transitionBetweenStates();
@@ -24,6 +24,7 @@ import { createMenuStateTrackingPlugin } from "./plugins/menu-state-tracking-plu
24
24
  import { createActionBarInteractionPlugin } from "./plugins/menu-action-interaction-plugin";
25
25
  import { createNodeSpec } from "../utils/plugin-factory";
26
26
  import { createTriggerPlugin } from "./plugins/trigger/factory";
27
+ import { isInlineImageTag, } from "../text-editor.types";
27
28
  import { getTableNodes, getTableEditingPlugins } from "./plugins/table-plugin";
28
29
  import { getImageNode, imageCache } from "./plugins/image/node";
29
30
  import { getMetadataFromDoc, hasMetadataChanged, } from "../utils/metadata-utils";
@@ -270,6 +271,7 @@ export class ProsemirrorAdapter {
270
271
  }
271
272
  componentWillLoad() {
272
273
  this.getActionBarItems();
274
+ this.validatedInlineImages = this.validateInlineImages();
273
275
  this.setupContentConverter();
274
276
  }
275
277
  componentDidLoad() {
@@ -295,7 +297,7 @@ export class ProsemirrorAdapter {
295
297
  (_e = this.view) === null || _e === void 0 ? void 0 : _e.destroy();
296
298
  }
297
299
  render() {
298
- return (h(Host, { key: '46d850f4bcc3d64ba180276ec9080f2b5b38470e', onFocus: this.handleFocus }, h("div", { key: '7695f4a898726c5e2db5d18257dda0e2e1616ed7', id: "editor" }), this.renderToolbar(), this.renderLinkMenu()));
300
+ return (h(Host, { key: '27949dced1fd61d1a80c42f58cf1699f05acb52e', onFocus: this.handleFocus }, h("div", { key: '75e941459326f0fb8f8d5042becd570e06cd99fa', id: "editor" }), this.renderToolbar(), this.renderLinkMenu()));
299
301
  }
300
302
  renderToolbar() {
301
303
  if (this.actionBarItems.length === 0 || this.ui === 'no-toolbar') {
@@ -309,12 +311,26 @@ export class ProsemirrorAdapter {
309
311
  }
310
312
  return (h("limel-portal", { containerId: this.portalId, visible: this.isLinkMenuOpen, openDirection: "top", inheritParentWidth: true, anchor: this.actionBarElement }, h("limel-text-editor-link-menu", { link: this.link, isOpen: this.isLinkMenuOpen, onLinkChange: this.handleLinkChange, onCancel: this.handleCancelLinkMenu, onSave: this.handleSaveLinkMenu })));
311
313
  }
314
+ validateInlineImages() {
315
+ const config = this.inlineImages;
316
+ // Require a hyphen: the tag name becomes a sanitizer whitelist key, so a
317
+ // built-in like `img` would hijack its rules and strip `src` from every
318
+ // real image. Custom element names always have a hyphen; built-ins never.
319
+ const isValidTagName = (name) => /^[a-z][a-z0-9._-]*$/.test(name) && name.includes('-');
320
+ if (config &&
321
+ isInlineImageTag(config) &&
322
+ !isValidTagName(config.tagName)) {
323
+ console.error(`inlineImages.tagName "${config.tagName}" is not a valid custom element name; ignoring inlineImages config.`);
324
+ return undefined;
325
+ }
326
+ return config;
327
+ }
312
328
  setupContentConverter() {
313
329
  if (this.contentType === 'markdown') {
314
- this.contentConverter = new MarkdownConverter(this.customElements, this.language);
330
+ this.contentConverter = new MarkdownConverter(this.customElements, this.language, this.validatedInlineImages);
315
331
  }
316
332
  else if (this.contentType === 'html') {
317
- this.contentConverter = new HTMLConverter(this.customElements);
333
+ this.contentConverter = new HTMLConverter(this.customElements, this.validatedInlineImages);
318
334
  }
319
335
  else {
320
336
  throw new Error(`Unsupported content type: ${this.contentType}. Only 'markdown' and 'html' are supported.`);
@@ -347,7 +363,7 @@ export class ProsemirrorAdapter {
347
363
  if (this.contentType === 'html') {
348
364
  nodes = nodes.append(getTableNodes());
349
365
  }
350
- nodes = nodes.append(getImageNode(this.language));
366
+ nodes = nodes.append(getImageNode(this.language, this.validatedInlineImages));
351
367
  return new Schema({
352
368
  nodes: nodes,
353
369
  marks: schema.spec.marks.append({
@@ -375,7 +391,7 @@ export class ProsemirrorAdapter {
375
391
  keymap(this.menuCommandFactory.buildKeymap()),
376
392
  createTriggerPlugin(this.triggerCharacters, this.contentConverter),
377
393
  createLinkPlugin(this.handleNewLinkSelection),
378
- createImageInserterPlugin(this.imagePasted.emit),
394
+ createImageInserterPlugin(this.imagePasted.emit, this.validatedInlineImages),
379
395
  createImageViewPlugin(this.language),
380
396
  createMenuStateTrackingPlugin(editorMenuTypesArray, this.menuCommandFactory, this.updateActiveActionBarItems),
381
397
  createActionBarInteractionPlugin(this.menuCommandFactory),
@@ -555,6 +571,36 @@ export class ProsemirrorAdapter {
555
571
  "setter": false,
556
572
  "defaultValue": "[]"
557
573
  },
574
+ "inlineImages": {
575
+ "type": "unknown",
576
+ "mutable": false,
577
+ "complexType": {
578
+ "original": "InlineImages",
579
+ "resolved": "InlineImageSrc | InlineImageTag",
580
+ "references": {
581
+ "InlineImages": {
582
+ "location": "import",
583
+ "path": "../text-editor.types",
584
+ "id": "src/components/text-editor/text-editor.types.ts::InlineImages",
585
+ "referenceLocation": "InlineImages"
586
+ }
587
+ }
588
+ },
589
+ "required": false,
590
+ "optional": true,
591
+ "docs": {
592
+ "tags": [{
593
+ "name": "private",
594
+ "text": undefined
595
+ }, {
596
+ "name": "alpha",
597
+ "text": undefined
598
+ }],
599
+ "text": "Configures inline image support. When set, the editor owns the paste +\nupload lifecycle and persists images either as the configured tag\ncarrying only a file id (`InlineImageTag`), or as a plain `<img src>`\n(`InlineImageSrc`)."
600
+ },
601
+ "getter": false,
602
+ "setter": false
603
+ },
558
604
  "triggerCharacters": {
559
605
  "type": "unknown",
560
606
  "mutable": false,
@@ -14,8 +14,8 @@ import { createRandomString } from "../../util/random-string";
14
14
  * @exampleComponent limel-example-text-editor-with-markdown
15
15
  * @exampleComponent limel-example-text-editor-with-html
16
16
  * @exampleComponent limel-example-text-editor-with-tables
17
- * @exampleComponent limel-example-text-editor-with-inline-images-file-storage
18
17
  * @exampleComponent limel-example-text-editor-with-inline-images-base64
18
+ * @exampleComponent limel-example-text-editor-with-inline-images-custom-tag
19
19
  * @exampleComponent limel-example-text-editor-allow-resize
20
20
  * @exampleComponent limel-example-text-editor-size
21
21
  * @exampleComponent limel-example-text-editor-ui
@@ -175,13 +175,13 @@ export class TextEditor {
175
175
  await ((_a = this.adapterElement) === null || _a === void 0 ? void 0 : _a.clear());
176
176
  }
177
177
  render() {
178
- return (h(Host, { key: '12f7643f82a4899efd112dc74e67fbd79be64d9f' }, h("limel-notched-outline", { key: '109e7c03874e141f073a05882ca48188adaced72', labelId: this.editorId, label: this.label, required: this.required, invalid: this.invalid, disabled: this.disabled, readonly: this.readonly, hasValue: !!this.value, hasFloatingLabel: true }, this.renderEditor(), this.renderPlaceholder()), this.renderHelperLine()));
178
+ return (h(Host, { key: '14e06e648e7625dd05901740f172c0b93bd1b494' }, h("limel-notched-outline", { key: '81acfc60c0b5a59175c33163e9436d17f27a480a', labelId: this.editorId, label: this.label, required: this.required, invalid: this.invalid, disabled: this.disabled, readonly: this.readonly, hasValue: !!this.value, hasFloatingLabel: true }, this.renderEditor(), this.renderPlaceholder()), this.renderHelperLine()));
179
179
  }
180
180
  renderEditor() {
181
181
  if (this.readonly) {
182
182
  return (h("limel-markdown", { slot: "content", value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId }));
183
183
  }
184
- return (h("limel-prosemirror-adapter", { ref: (el) => (this.adapterElement = el), slot: "content", "aria-placeholder": this.placeholder, contentType: this.contentType, onChange: this.handleChange, onImagePasted: this.handleImagePasted, onImageRemoved: this.handleImageRemoved, onMetadataChange: this.handleMetadataChange, customElements: this.customElements, value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId, "aria-disabled": this.disabled, "aria-invalid": this.invalid, "aria-required": this.required, language: this.language, triggerCharacters: this.triggers, disabled: this.disabled, ui: this.ui }));
184
+ return (h("limel-prosemirror-adapter", { ref: (el) => (this.adapterElement = el), slot: "content", "aria-placeholder": this.placeholder, contentType: this.contentType, onChange: this.handleChange, onImagePasted: this.handleImagePasted, onImageRemoved: this.handleImageRemoved, onMetadataChange: this.handleMetadataChange, customElements: this.customElements, inlineImages: this.inlineImages, value: this.value, "aria-controls": this.helperText ? this.helperTextId : undefined, id: this.editorId, "aria-disabled": this.disabled, "aria-invalid": this.invalid, "aria-required": this.required, language: this.language, triggerCharacters: this.triggers, disabled: this.disabled, ui: this.ui }));
185
185
  }
186
186
  renderPlaceholder() {
187
187
  if (!this.placeholder || this.value) {
@@ -418,6 +418,36 @@ export class TextEditor {
418
418
  "setter": false,
419
419
  "defaultValue": "[]"
420
420
  },
421
+ "inlineImages": {
422
+ "type": "unknown",
423
+ "mutable": false,
424
+ "complexType": {
425
+ "original": "InlineImages",
426
+ "resolved": "InlineImageSrc | InlineImageTag",
427
+ "references": {
428
+ "InlineImages": {
429
+ "location": "import",
430
+ "path": "./text-editor.types",
431
+ "id": "src/components/text-editor/text-editor.types.ts::InlineImages",
432
+ "referenceLocation": "InlineImages"
433
+ }
434
+ }
435
+ },
436
+ "required": false,
437
+ "optional": true,
438
+ "docs": {
439
+ "tags": [{
440
+ "name": "private",
441
+ "text": undefined
442
+ }, {
443
+ "name": "alpha",
444
+ "text": undefined
445
+ }],
446
+ "text": "Configures inline image support: the editor owns the paste lifecycle and\nresize, the consumer owns upload, storage format, and URL resolution."
447
+ },
448
+ "getter": false,
449
+ "setter": false
450
+ },
421
451
  "triggers": {
422
452
  "type": "unknown",
423
453
  "mutable": false,
@@ -1 +1,10 @@
1
- export {};
1
+ /**
2
+ * Narrows `InlineImages` to its tag shape, exposing `tagName`/`getUrl`.
3
+ *
4
+ * @param config - the inline-images configuration to test
5
+ * @returns whether images are persisted as a custom id-carrying element
6
+ * @alpha
7
+ */
8
+ export function isInlineImageTag(config) {
9
+ return 'tagName' in config;
10
+ }
@@ -1 +1,30 @@
1
- export {};
1
+ import { isInlineImageTag } from "../text-editor.types";
2
+ import { INLINE_IMAGE_ATTRIBUTES } from "../prosemirror-adapter/plugins/image/node";
3
+ /**
4
+ * Build the sanitization whitelist for editor content: the consumer's custom
5
+ * elements, plus (when inline images are configured) the inline-image tag so it
6
+ * survives parsing into an image node. Shared by all content converters.
7
+ *
8
+ * @param customNodes
9
+ * @param inlineImages
10
+ * @private
11
+ */
12
+ export function buildContentWhitelist(customNodes, inlineImages) {
13
+ if (!inlineImages || !isInlineImageTag(inlineImages)) {
14
+ return customNodes;
15
+ }
16
+ // Guard the boundary independently of the upstream `validateInlineImages`
17
+ // check: the tag name becomes a sanitizer whitelist key, so a built-in like
18
+ // `img` (no hyphen) would hijack its rules and strip `src` from every real
19
+ // image. Custom element names always contain a hyphen; built-ins never do.
20
+ if (!inlineImages.tagName.includes('-')) {
21
+ return customNodes;
22
+ }
23
+ return [
24
+ ...customNodes,
25
+ {
26
+ tagName: inlineImages.tagName,
27
+ attributes: [...INLINE_IMAGE_ATTRIBUTES],
28
+ },
29
+ ];
30
+ }
@@ -1,3 +1,4 @@
1
+ import { buildContentWhitelist, } from "./content-type-converter";
1
2
  import { sanitizeHTML } from "../../markdown/markdown-parser";
2
3
  import { DOMSerializer } from "prosemirror-model";
3
4
  import { hasImageNode } from "../prosemirror-adapter/plugins/image/node";
@@ -6,9 +7,9 @@ import { hasCustomElementNode } from "./has-custom-element-node";
6
7
  * @private
7
8
  */
8
9
  export class HTMLConverter {
9
- constructor(plugins) {
10
+ constructor(plugins, inlineImages) {
10
11
  this.parseAsHTML = (text) => {
11
- return sanitizeHTML(text, this.customNodes);
12
+ return sanitizeHTML(text, buildContentWhitelist(this.customNodes, this.inlineImages));
12
13
  };
13
14
  this.serialize = (view) => {
14
15
  if (view.state.doc.textContent.trim() === '' &&
@@ -21,5 +22,6 @@ export class HTMLConverter {
21
22
  return div.innerHTML;
22
23
  };
23
24
  this.customNodes = plugins;
25
+ this.inlineImages = inlineImages;
24
26
  }
25
27
  }
@@ -1,3 +1,4 @@
1
+ import { buildContentWhitelist, } from "./content-type-converter";
1
2
  import { MarkdownSerializer, defaultMarkdownSerializer, } from "prosemirror-markdown";
2
3
  import { markdownToHTML } from "../../markdown/markdown-parser";
3
4
  import { getImageNodeMarkdownSerializer, hasImageNode, } from "../prosemirror-adapter/plugins/image/node";
@@ -15,12 +16,12 @@ const createMarkdownSerializerFunction = (config) => {
15
16
  state.write(tagClose);
16
17
  };
17
18
  };
18
- const buildMarkdownSerializer = (plugins, language) => {
19
+ const buildMarkdownSerializer = (plugins, language, inlineImages) => {
19
20
  const customNodes = {};
20
21
  for (const plugin of plugins) {
21
22
  customNodes[plugin.tagName] = createMarkdownSerializerFunction(plugin);
22
23
  }
23
- const nodes = Object.assign(Object.assign(Object.assign({}, defaultMarkdownSerializer.nodes), getImageNodeMarkdownSerializer(language)), customNodes);
24
+ const nodes = Object.assign(Object.assign(Object.assign({}, defaultMarkdownSerializer.nodes), getImageNodeMarkdownSerializer(language, inlineImages)), customNodes);
24
25
  const marks = Object.assign(Object.assign({}, defaultMarkdownSerializer.marks), { strikethrough: {
25
26
  open: '~~',
26
27
  close: '~~',
@@ -33,9 +34,11 @@ const buildMarkdownSerializer = (plugins, language) => {
33
34
  * @private
34
35
  */
35
36
  export class MarkdownConverter {
36
- constructor(plugins, language) {
37
+ constructor(plugins, language, inlineImages) {
37
38
  this.parseAsHTML = (text) => {
38
- return markdownToHTML(text, { whitelist: this.customNodes });
39
+ return markdownToHTML(text, {
40
+ whitelist: buildContentWhitelist(this.customNodes, this.inlineImages),
41
+ });
39
42
  };
40
43
  this.serialize = (view) => {
41
44
  if (view.state.doc.textContent.trim() === '' &&
@@ -45,7 +48,8 @@ export class MarkdownConverter {
45
48
  }
46
49
  return this.markdownSerializer.serialize(view.state.doc);
47
50
  };
48
- this.markdownSerializer = buildMarkdownSerializer(plugins, language);
51
+ this.markdownSerializer = buildMarkdownSerializer(plugins, language, inlineImages);
49
52
  this.customNodes = plugins;
53
+ this.inlineImages = inlineImages;
50
54
  }
51
55
  }
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { g as globalConfig } from './config-Dnt5w_Bp.js';
2
2
  export { _ as _mapLayout } from './layout-C4zsCPOF.js';
3
- export { E as EditorMenuTypes, L as LevelMapping, M as MouseButtons, e as editorMenuTypesArray } from './types-BWYo6dLf.js';
3
+ export { E as EditorMenuTypes, L as LevelMapping, M as MouseButtons, e as editorMenuTypesArray, i as isInlineImageTag } from './types-C_Yu8dOg.js';
4
4
  export { r as resizeImage } from './image-resize-DPcww8rE.js';
5
5
  export { r as redrawComponents } from './dispatch-resize-event-z_E3sq3p.js';
6
6