@railway/inkwell 1.4.0 → 2.1.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/dist/index.cjs CHANGED
@@ -368,6 +368,73 @@ function computeInlineDecorations(entry) {
368
368
  strikeMarker: true
369
369
  });
370
370
  }
371
+ const linkRanges = [];
372
+ const linkRegex = /(?<!!)\[([^\]\n]+)\]\(([^)\s]+)\)/g;
373
+ while ((match = linkRegex.exec(text)) !== null) {
374
+ if (isInCode(match.index)) continue;
375
+ const start = match.index;
376
+ const end = start + match[0].length;
377
+ const labelLen = match[1].length;
378
+ const urlLen = match[2].length;
379
+ const openBracket = start;
380
+ const labelStart = start + 1;
381
+ const labelEnd = labelStart + labelLen;
382
+ const closeBracket = labelEnd;
383
+ const openParen = closeBracket + 1;
384
+ const urlStart = openParen + 1;
385
+ const urlEnd = urlStart + urlLen;
386
+ const closeParen = urlEnd;
387
+ linkRanges.push({ start, end });
388
+ ranges.push({
389
+ anchor: { path: [...path, 0], offset: openBracket },
390
+ focus: { path: [...path, 0], offset: openBracket + 1 },
391
+ linkMarker: true
392
+ });
393
+ ranges.push({
394
+ anchor: { path: [...path, 0], offset: labelStart },
395
+ focus: { path: [...path, 0], offset: labelEnd },
396
+ link: true
397
+ });
398
+ ranges.push({
399
+ anchor: { path: [...path, 0], offset: closeBracket },
400
+ focus: { path: [...path, 0], offset: closeBracket + 1 },
401
+ linkMarker: true
402
+ });
403
+ ranges.push({
404
+ anchor: { path: [...path, 0], offset: openParen },
405
+ focus: { path: [...path, 0], offset: openParen + 1 },
406
+ linkMarker: true
407
+ });
408
+ ranges.push({
409
+ anchor: { path: [...path, 0], offset: urlStart },
410
+ focus: { path: [...path, 0], offset: urlEnd },
411
+ linkUrl: true
412
+ });
413
+ ranges.push({
414
+ anchor: { path: [...path, 0], offset: closeParen },
415
+ focus: { path: [...path, 0], offset: closeParen + 1 },
416
+ linkMarker: true
417
+ });
418
+ }
419
+ const isInLink = (offset) => linkRanges.some((r) => offset >= r.start && offset < r.end);
420
+ const urlRegex = /(?:https?:\/\/|www\.)[^\s<>()[\]]+/g;
421
+ while ((match = urlRegex.exec(text)) !== null) {
422
+ if (isInCode(match.index)) continue;
423
+ if (isInLink(match.index)) continue;
424
+ let matched = match[0];
425
+ let start = match.index;
426
+ let end = start + matched.length;
427
+ while (matched.length > 0 && /[.,;:!?]/.test(matched[matched.length - 1])) {
428
+ matched = matched.slice(0, -1);
429
+ end--;
430
+ }
431
+ if (matched.length === 0) continue;
432
+ ranges.push({
433
+ anchor: { path: [...path, 0], offset: start },
434
+ focus: { path: [...path, 0], offset: end },
435
+ link: true
436
+ });
437
+ }
371
438
  return ranges;
372
439
  }
373
440
  function computeFenceDecorations(entry) {
@@ -775,17 +842,30 @@ function ImageElement({
775
842
  }
776
843
  function RenderLeaf({ attributes, children, leaf }) {
777
844
  const l = leaf;
778
- if (l.boldMarker || l.italicMarker || l.strikeMarker) {
845
+ if (l.boldMarker || l.italicMarker || l.strikeMarker || l.linkMarker) {
779
846
  return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, className: editorClass("marker"), children });
780
847
  }
781
848
  if (l.codeMarker) {
782
849
  return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, className: editorClass("backtick"), children });
783
850
  }
851
+ if (l.linkUrl) {
852
+ return /* @__PURE__ */ jsxRuntime.jsx(
853
+ "span",
854
+ {
855
+ ...attributes,
856
+ className: `${editorClass("marker")} ${editorClass("link-url")}`,
857
+ children
858
+ }
859
+ );
860
+ }
784
861
  let content = children;
785
862
  if (l.bold) content = /* @__PURE__ */ jsxRuntime.jsx("strong", { children: content });
786
863
  if (l.italic) content = /* @__PURE__ */ jsxRuntime.jsx("em", { children: content });
787
864
  if (l.strikethrough) content = /* @__PURE__ */ jsxRuntime.jsx("del", { children: content });
788
865
  if (l.inlineCode) content = /* @__PURE__ */ jsxRuntime.jsx("code", { children: content });
866
+ if (l.link) {
867
+ content = /* @__PURE__ */ jsxRuntime.jsx("span", { className: editorClass("link"), children: content });
868
+ }
789
869
  if (l.hljs) {
790
870
  content = /* @__PURE__ */ jsxRuntime.jsx("span", { className: l.hljs, children: content });
791
871
  }
@@ -827,6 +907,7 @@ var HEADING_RE2 = /^#{1,6}$/;
827
907
  var UNORDERED_LIST_CONTINUE_RE = /^(\s*)([-*+]) \S/;
828
908
  var UNORDERED_LIST_EMPTY_RE = /^(\s*)([-*+]) ?$/;
829
909
  var HEADING_LINE_RE = /^(#{1,6})\s/;
910
+ var PASTED_URL_RE = /^(?:https?:\/\/|www\.)\S+$/i;
830
911
  function classifyLine(text, deco) {
831
912
  const headingMatch = HEADING_LINE_RE.exec(text);
832
913
  if (headingMatch) {
@@ -1161,6 +1242,14 @@ function withMarkdown(editor, featuresRef) {
1161
1242
  editor.insertData = (data) => {
1162
1243
  const text = data.getData("text/plain");
1163
1244
  if (text) {
1245
+ const trimmed = text.trim();
1246
+ const sel = editor.selection;
1247
+ if (PASTED_URL_RE.test(trimmed) && sel && !slate.Range.isCollapsed(sel) && slate.Editor.string(editor, sel).length > 0) {
1248
+ const selectedText = slate.Editor.string(editor, sel);
1249
+ slate.Transforms.delete(editor);
1250
+ slate.Transforms.insertText(editor, `[${selectedText}](${trimmed})`);
1251
+ return;
1252
+ }
1164
1253
  const nodes = deserialize(text, featuresRef.current);
1165
1254
  slate.Transforms.insertNodes(editor, nodes);
1166
1255
  return;
@@ -3163,6 +3252,97 @@ function CopyCodeBlock({
3163
3252
  /* @__PURE__ */ jsxRuntime.jsx("pre", { ref: preRef, ...props, children })
3164
3253
  ] });
3165
3254
  }
3255
+ function rehypeTrimCodeBlockTrailingNewline() {
3256
+ return (tree) => {
3257
+ unistUtilVisit.visit(tree, "element", (node, _index, parent) => {
3258
+ if (node.tagName !== "code") return;
3259
+ if (!parent || parent.type !== "element" || parent.tagName !== "pre") {
3260
+ return;
3261
+ }
3262
+ const spine = [node];
3263
+ let cursor = node;
3264
+ while (cursor.type === "element") {
3265
+ const children = cursor.children;
3266
+ if (!children.length) return;
3267
+ const last = children[children.length - 1];
3268
+ if (last.type !== "element" && last.type !== "text") return;
3269
+ cursor = last;
3270
+ if (cursor.type === "element") spine.push(cursor);
3271
+ }
3272
+ if (!cursor.value.endsWith("\n")) return;
3273
+ cursor.value = cursor.value.slice(0, -1);
3274
+ if (cursor.value === "") {
3275
+ const owner = spine[spine.length - 1];
3276
+ owner.children.pop();
3277
+ }
3278
+ });
3279
+ };
3280
+ }
3281
+ function splitTextOnNewlines(text) {
3282
+ if (!text.value.includes("\n")) return [text];
3283
+ const parts = text.value.split("\n");
3284
+ const result = [];
3285
+ for (let i = 0; i < parts.length; i++) {
3286
+ if (parts[i] !== "") {
3287
+ result.push({ type: "text", value: parts[i] });
3288
+ }
3289
+ if (i < parts.length - 1) {
3290
+ result.push({ type: "break" });
3291
+ }
3292
+ }
3293
+ return result;
3294
+ }
3295
+ function expandParagraphChildren(children) {
3296
+ let changed = false;
3297
+ const next = [];
3298
+ for (const child of children) {
3299
+ if (child.type === "text" && child.value.includes("\n")) {
3300
+ next.push(...splitTextOnNewlines(child));
3301
+ changed = true;
3302
+ } else {
3303
+ next.push(child);
3304
+ }
3305
+ }
3306
+ return changed ? next : null;
3307
+ }
3308
+ function remarkSoftBreakAsBreak() {
3309
+ return (tree) => {
3310
+ unistUtilVisit.visit(tree, "paragraph", (node) => {
3311
+ const expanded = expandParagraphChildren(node.children);
3312
+ if (expanded) node.children = expanded;
3313
+ });
3314
+ };
3315
+ }
3316
+ function remarkSoftBreakAsParagraph() {
3317
+ return (tree) => {
3318
+ unistUtilVisit.visit(tree, "paragraph", (node, index, parent) => {
3319
+ if (!parent || index == null) return;
3320
+ const expanded = expandParagraphChildren(node.children) ?? node.children.slice();
3321
+ const breakIndices = [];
3322
+ for (let i = 0; i < expanded.length; i++) {
3323
+ if (expanded[i].type === "break") breakIndices.push(i);
3324
+ }
3325
+ if (breakIndices.length === 0) {
3326
+ return;
3327
+ }
3328
+ const newParagraphs = [];
3329
+ let start = 0;
3330
+ for (const breakIdx of [...breakIndices, expanded.length]) {
3331
+ if (breakIdx > start) {
3332
+ newParagraphs.push({
3333
+ type: "paragraph",
3334
+ children: expanded.slice(start, breakIdx)
3335
+ });
3336
+ }
3337
+ start = breakIdx + 1;
3338
+ }
3339
+ parent.children.splice(index, 1, ...newParagraphs);
3340
+ return [unistUtilVisit.SKIP, index + newParagraphs.length];
3341
+ });
3342
+ };
3343
+ }
3344
+
3345
+ // src/renderer/markdown-parser.ts
3166
3346
  var MENTION_TAG_PREFIX = "inkwell-mention-";
3167
3347
  function rehypeMentions(mentions) {
3168
3348
  return () => (tree) => {
@@ -3225,7 +3405,14 @@ function rehypeMentions(mentions) {
3225
3405
  };
3226
3406
  }
3227
3407
  function createProcessor2(options = {}) {
3228
- const proc = unified.unified().use(remarkParse__default.default).use(remarkGfm__default.default).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype__default.default);
3408
+ const proc = unified.unified().use(remarkParse__default.default).use(remarkGfm__default.default).use(remarkNoTables).use(remarkFlattenBlockquotes);
3409
+ const softBreak = options.softBreak ?? "paragraph";
3410
+ if (softBreak === "br") {
3411
+ proc.use(remarkSoftBreakAsBreak);
3412
+ } else if (softBreak === "paragraph") {
3413
+ proc.use(remarkSoftBreakAsParagraph);
3414
+ }
3415
+ proc.use(remarkRehype__default.default);
3229
3416
  const plugins = options.rehypePlugins ?? [
3230
3417
  [rehypeHighlight__default.default, { detect: true }]
3231
3418
  ];
@@ -3237,6 +3424,7 @@ function createProcessor2(options = {}) {
3237
3424
  proc.use(plugin);
3238
3425
  }
3239
3426
  }
3427
+ proc.use(rehypeTrimCodeBlockTrailingNewline);
3240
3428
  proc.use(rehypeSanitize__default.default, {
3241
3429
  ...rehypeSanitize.defaultSchema,
3242
3430
  tagNames: [...rehypeSanitize.defaultSchema.tagNames ?? [], "span"],
@@ -3284,7 +3472,8 @@ function InkwellRenderer({
3284
3472
  className,
3285
3473
  components,
3286
3474
  rehypePlugins,
3287
- mentions
3475
+ mentions,
3476
+ softBreak
3288
3477
  }) {
3289
3478
  const mergedComponents = react.useMemo(
3290
3479
  () => ({ pre: CopyCodeBlock, ...components }),
@@ -3294,9 +3483,10 @@ function InkwellRenderer({
3294
3483
  () => parseMarkdown(content, {
3295
3484
  components: mergedComponents,
3296
3485
  rehypePlugins,
3297
- mentions
3486
+ mentions,
3487
+ softBreak
3298
3488
  }),
3299
- [content, mergedComponents, rehypePlugins, mentions]
3489
+ [content, mergedComponents, rehypePlugins, mentions, softBreak]
3300
3490
  );
3301
3491
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `inkwell-renderer ${className ?? ""}`, children: rendered });
3302
3492
  }
package/dist/index.d.cts CHANGED
@@ -110,6 +110,16 @@ interface InkwellEditorProps {
110
110
  /** Called when submitOnEnter handles Enter. */
111
111
  onSubmit?: (content: string) => void;
112
112
  }
113
+ /**
114
+ * How `InkwellRenderer` handles single-newline soft breaks in source markdown.
115
+ * - `"paragraph"` (default): split the enclosing paragraph at the soft break,
116
+ * producing two `<p>` elements with normal paragraph margins.
117
+ * - `"br"`: emit a `<br />`. Matches GFM-style behavior (and Showdown with
118
+ * `simpleLineBreaks: true`).
119
+ * - `"preserve"`: keep as a literal `\n` text node. The browser collapses it
120
+ * to whitespace per CSS. Matches strict CommonMark.
121
+ */
122
+ type InkwellSoftBreakBehavior = "preserve" | "br" | "paragraph";
113
123
  interface InkwellRendererProps {
114
124
  /** Markdown source content string. */
115
125
  content: string;
@@ -121,11 +131,15 @@ interface InkwellRendererProps {
121
131
  rehypePlugins?: RehypePluginConfig[];
122
132
  /** Mention patterns to expand in rendered text. */
123
133
  mentions?: MentionRenderer[];
134
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
135
+ softBreak?: InkwellSoftBreakBehavior;
124
136
  }
125
137
  interface ParseMarkdownOptions {
126
138
  components?: InkwellComponents;
127
139
  rehypePlugins?: RehypePluginConfig[];
128
140
  mentions?: MentionRenderer[];
141
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
142
+ softBreak?: InkwellSoftBreakBehavior;
129
143
  }
130
144
  interface MentionRenderer {
131
145
  /** Regular expression applied to text-node content. */
@@ -482,11 +496,11 @@ declare function createSnippetsPlugin({ snippets, name, trigger, }: SnippetsPlug
482
496
  */
483
497
  declare function htmlToMarkdown(html: string): string;
484
498
 
485
- declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, }: InkwellRendererProps): ReactNode;
499
+ declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, softBreak, }: InkwellRendererProps): ReactNode;
486
500
 
487
501
  /**
488
502
  * Parse a markdown string into React elements synchronously
489
503
  */
490
504
  declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
491
505
 
492
- export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
506
+ export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type InkwellSoftBreakBehavior, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
package/dist/index.d.ts CHANGED
@@ -110,6 +110,16 @@ interface InkwellEditorProps {
110
110
  /** Called when submitOnEnter handles Enter. */
111
111
  onSubmit?: (content: string) => void;
112
112
  }
113
+ /**
114
+ * How `InkwellRenderer` handles single-newline soft breaks in source markdown.
115
+ * - `"paragraph"` (default): split the enclosing paragraph at the soft break,
116
+ * producing two `<p>` elements with normal paragraph margins.
117
+ * - `"br"`: emit a `<br />`. Matches GFM-style behavior (and Showdown with
118
+ * `simpleLineBreaks: true`).
119
+ * - `"preserve"`: keep as a literal `\n` text node. The browser collapses it
120
+ * to whitespace per CSS. Matches strict CommonMark.
121
+ */
122
+ type InkwellSoftBreakBehavior = "preserve" | "br" | "paragraph";
113
123
  interface InkwellRendererProps {
114
124
  /** Markdown source content string. */
115
125
  content: string;
@@ -121,11 +131,15 @@ interface InkwellRendererProps {
121
131
  rehypePlugins?: RehypePluginConfig[];
122
132
  /** Mention patterns to expand in rendered text. */
123
133
  mentions?: MentionRenderer[];
134
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
135
+ softBreak?: InkwellSoftBreakBehavior;
124
136
  }
125
137
  interface ParseMarkdownOptions {
126
138
  components?: InkwellComponents;
127
139
  rehypePlugins?: RehypePluginConfig[];
128
140
  mentions?: MentionRenderer[];
141
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
142
+ softBreak?: InkwellSoftBreakBehavior;
129
143
  }
130
144
  interface MentionRenderer {
131
145
  /** Regular expression applied to text-node content. */
@@ -482,11 +496,11 @@ declare function createSnippetsPlugin({ snippets, name, trigger, }: SnippetsPlug
482
496
  */
483
497
  declare function htmlToMarkdown(html: string): string;
484
498
 
485
- declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, }: InkwellRendererProps): ReactNode;
499
+ declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, softBreak, }: InkwellRendererProps): ReactNode;
486
500
 
487
501
  /**
488
502
  * Parse a markdown string into React elements synchronously
489
503
  */
490
504
  declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
491
505
 
492
- export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
506
+ export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type InkwellSoftBreakBehavior, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
package/dist/index.js CHANGED
@@ -353,6 +353,73 @@ function computeInlineDecorations(entry) {
353
353
  strikeMarker: true
354
354
  });
355
355
  }
356
+ const linkRanges = [];
357
+ const linkRegex = /(?<!!)\[([^\]\n]+)\]\(([^)\s]+)\)/g;
358
+ while ((match = linkRegex.exec(text)) !== null) {
359
+ if (isInCode(match.index)) continue;
360
+ const start = match.index;
361
+ const end = start + match[0].length;
362
+ const labelLen = match[1].length;
363
+ const urlLen = match[2].length;
364
+ const openBracket = start;
365
+ const labelStart = start + 1;
366
+ const labelEnd = labelStart + labelLen;
367
+ const closeBracket = labelEnd;
368
+ const openParen = closeBracket + 1;
369
+ const urlStart = openParen + 1;
370
+ const urlEnd = urlStart + urlLen;
371
+ const closeParen = urlEnd;
372
+ linkRanges.push({ start, end });
373
+ ranges.push({
374
+ anchor: { path: [...path, 0], offset: openBracket },
375
+ focus: { path: [...path, 0], offset: openBracket + 1 },
376
+ linkMarker: true
377
+ });
378
+ ranges.push({
379
+ anchor: { path: [...path, 0], offset: labelStart },
380
+ focus: { path: [...path, 0], offset: labelEnd },
381
+ link: true
382
+ });
383
+ ranges.push({
384
+ anchor: { path: [...path, 0], offset: closeBracket },
385
+ focus: { path: [...path, 0], offset: closeBracket + 1 },
386
+ linkMarker: true
387
+ });
388
+ ranges.push({
389
+ anchor: { path: [...path, 0], offset: openParen },
390
+ focus: { path: [...path, 0], offset: openParen + 1 },
391
+ linkMarker: true
392
+ });
393
+ ranges.push({
394
+ anchor: { path: [...path, 0], offset: urlStart },
395
+ focus: { path: [...path, 0], offset: urlEnd },
396
+ linkUrl: true
397
+ });
398
+ ranges.push({
399
+ anchor: { path: [...path, 0], offset: closeParen },
400
+ focus: { path: [...path, 0], offset: closeParen + 1 },
401
+ linkMarker: true
402
+ });
403
+ }
404
+ const isInLink = (offset) => linkRanges.some((r) => offset >= r.start && offset < r.end);
405
+ const urlRegex = /(?:https?:\/\/|www\.)[^\s<>()[\]]+/g;
406
+ while ((match = urlRegex.exec(text)) !== null) {
407
+ if (isInCode(match.index)) continue;
408
+ if (isInLink(match.index)) continue;
409
+ let matched = match[0];
410
+ let start = match.index;
411
+ let end = start + matched.length;
412
+ while (matched.length > 0 && /[.,;:!?]/.test(matched[matched.length - 1])) {
413
+ matched = matched.slice(0, -1);
414
+ end--;
415
+ }
416
+ if (matched.length === 0) continue;
417
+ ranges.push({
418
+ anchor: { path: [...path, 0], offset: start },
419
+ focus: { path: [...path, 0], offset: end },
420
+ link: true
421
+ });
422
+ }
356
423
  return ranges;
357
424
  }
358
425
  function computeFenceDecorations(entry) {
@@ -760,17 +827,30 @@ function ImageElement({
760
827
  }
761
828
  function RenderLeaf({ attributes, children, leaf }) {
762
829
  const l = leaf;
763
- if (l.boldMarker || l.italicMarker || l.strikeMarker) {
830
+ if (l.boldMarker || l.italicMarker || l.strikeMarker || l.linkMarker) {
764
831
  return /* @__PURE__ */ jsx("span", { ...attributes, className: editorClass("marker"), children });
765
832
  }
766
833
  if (l.codeMarker) {
767
834
  return /* @__PURE__ */ jsx("span", { ...attributes, className: editorClass("backtick"), children });
768
835
  }
836
+ if (l.linkUrl) {
837
+ return /* @__PURE__ */ jsx(
838
+ "span",
839
+ {
840
+ ...attributes,
841
+ className: `${editorClass("marker")} ${editorClass("link-url")}`,
842
+ children
843
+ }
844
+ );
845
+ }
769
846
  let content = children;
770
847
  if (l.bold) content = /* @__PURE__ */ jsx("strong", { children: content });
771
848
  if (l.italic) content = /* @__PURE__ */ jsx("em", { children: content });
772
849
  if (l.strikethrough) content = /* @__PURE__ */ jsx("del", { children: content });
773
850
  if (l.inlineCode) content = /* @__PURE__ */ jsx("code", { children: content });
851
+ if (l.link) {
852
+ content = /* @__PURE__ */ jsx("span", { className: editorClass("link"), children: content });
853
+ }
774
854
  if (l.hljs) {
775
855
  content = /* @__PURE__ */ jsx("span", { className: l.hljs, children: content });
776
856
  }
@@ -812,6 +892,7 @@ var HEADING_RE2 = /^#{1,6}$/;
812
892
  var UNORDERED_LIST_CONTINUE_RE = /^(\s*)([-*+]) \S/;
813
893
  var UNORDERED_LIST_EMPTY_RE = /^(\s*)([-*+]) ?$/;
814
894
  var HEADING_LINE_RE = /^(#{1,6})\s/;
895
+ var PASTED_URL_RE = /^(?:https?:\/\/|www\.)\S+$/i;
815
896
  function classifyLine(text, deco) {
816
897
  const headingMatch = HEADING_LINE_RE.exec(text);
817
898
  if (headingMatch) {
@@ -1146,6 +1227,14 @@ function withMarkdown(editor, featuresRef) {
1146
1227
  editor.insertData = (data) => {
1147
1228
  const text = data.getData("text/plain");
1148
1229
  if (text) {
1230
+ const trimmed = text.trim();
1231
+ const sel = editor.selection;
1232
+ if (PASTED_URL_RE.test(trimmed) && sel && !Range.isCollapsed(sel) && Editor.string(editor, sel).length > 0) {
1233
+ const selectedText = Editor.string(editor, sel);
1234
+ Transforms.delete(editor);
1235
+ Transforms.insertText(editor, `[${selectedText}](${trimmed})`);
1236
+ return;
1237
+ }
1149
1238
  const nodes = deserialize(text, featuresRef.current);
1150
1239
  Transforms.insertNodes(editor, nodes);
1151
1240
  return;
@@ -3148,6 +3237,97 @@ function CopyCodeBlock({
3148
3237
  /* @__PURE__ */ jsx("pre", { ref: preRef, ...props, children })
3149
3238
  ] });
3150
3239
  }
3240
+ function rehypeTrimCodeBlockTrailingNewline() {
3241
+ return (tree) => {
3242
+ visit(tree, "element", (node, _index, parent) => {
3243
+ if (node.tagName !== "code") return;
3244
+ if (!parent || parent.type !== "element" || parent.tagName !== "pre") {
3245
+ return;
3246
+ }
3247
+ const spine = [node];
3248
+ let cursor = node;
3249
+ while (cursor.type === "element") {
3250
+ const children = cursor.children;
3251
+ if (!children.length) return;
3252
+ const last = children[children.length - 1];
3253
+ if (last.type !== "element" && last.type !== "text") return;
3254
+ cursor = last;
3255
+ if (cursor.type === "element") spine.push(cursor);
3256
+ }
3257
+ if (!cursor.value.endsWith("\n")) return;
3258
+ cursor.value = cursor.value.slice(0, -1);
3259
+ if (cursor.value === "") {
3260
+ const owner = spine[spine.length - 1];
3261
+ owner.children.pop();
3262
+ }
3263
+ });
3264
+ };
3265
+ }
3266
+ function splitTextOnNewlines(text) {
3267
+ if (!text.value.includes("\n")) return [text];
3268
+ const parts = text.value.split("\n");
3269
+ const result = [];
3270
+ for (let i = 0; i < parts.length; i++) {
3271
+ if (parts[i] !== "") {
3272
+ result.push({ type: "text", value: parts[i] });
3273
+ }
3274
+ if (i < parts.length - 1) {
3275
+ result.push({ type: "break" });
3276
+ }
3277
+ }
3278
+ return result;
3279
+ }
3280
+ function expandParagraphChildren(children) {
3281
+ let changed = false;
3282
+ const next = [];
3283
+ for (const child of children) {
3284
+ if (child.type === "text" && child.value.includes("\n")) {
3285
+ next.push(...splitTextOnNewlines(child));
3286
+ changed = true;
3287
+ } else {
3288
+ next.push(child);
3289
+ }
3290
+ }
3291
+ return changed ? next : null;
3292
+ }
3293
+ function remarkSoftBreakAsBreak() {
3294
+ return (tree) => {
3295
+ visit(tree, "paragraph", (node) => {
3296
+ const expanded = expandParagraphChildren(node.children);
3297
+ if (expanded) node.children = expanded;
3298
+ });
3299
+ };
3300
+ }
3301
+ function remarkSoftBreakAsParagraph() {
3302
+ return (tree) => {
3303
+ visit(tree, "paragraph", (node, index, parent) => {
3304
+ if (!parent || index == null) return;
3305
+ const expanded = expandParagraphChildren(node.children) ?? node.children.slice();
3306
+ const breakIndices = [];
3307
+ for (let i = 0; i < expanded.length; i++) {
3308
+ if (expanded[i].type === "break") breakIndices.push(i);
3309
+ }
3310
+ if (breakIndices.length === 0) {
3311
+ return;
3312
+ }
3313
+ const newParagraphs = [];
3314
+ let start = 0;
3315
+ for (const breakIdx of [...breakIndices, expanded.length]) {
3316
+ if (breakIdx > start) {
3317
+ newParagraphs.push({
3318
+ type: "paragraph",
3319
+ children: expanded.slice(start, breakIdx)
3320
+ });
3321
+ }
3322
+ start = breakIdx + 1;
3323
+ }
3324
+ parent.children.splice(index, 1, ...newParagraphs);
3325
+ return [SKIP, index + newParagraphs.length];
3326
+ });
3327
+ };
3328
+ }
3329
+
3330
+ // src/renderer/markdown-parser.ts
3151
3331
  var MENTION_TAG_PREFIX = "inkwell-mention-";
3152
3332
  function rehypeMentions(mentions) {
3153
3333
  return () => (tree) => {
@@ -3210,7 +3390,14 @@ function rehypeMentions(mentions) {
3210
3390
  };
3211
3391
  }
3212
3392
  function createProcessor2(options = {}) {
3213
- const proc = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype);
3393
+ const proc = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes);
3394
+ const softBreak = options.softBreak ?? "paragraph";
3395
+ if (softBreak === "br") {
3396
+ proc.use(remarkSoftBreakAsBreak);
3397
+ } else if (softBreak === "paragraph") {
3398
+ proc.use(remarkSoftBreakAsParagraph);
3399
+ }
3400
+ proc.use(remarkRehype);
3214
3401
  const plugins = options.rehypePlugins ?? [
3215
3402
  [rehypeHighlight, { detect: true }]
3216
3403
  ];
@@ -3222,6 +3409,7 @@ function createProcessor2(options = {}) {
3222
3409
  proc.use(plugin);
3223
3410
  }
3224
3411
  }
3412
+ proc.use(rehypeTrimCodeBlockTrailingNewline);
3225
3413
  proc.use(rehypeSanitize, {
3226
3414
  ...defaultSchema,
3227
3415
  tagNames: [...defaultSchema.tagNames ?? [], "span"],
@@ -3269,7 +3457,8 @@ function InkwellRenderer({
3269
3457
  className,
3270
3458
  components,
3271
3459
  rehypePlugins,
3272
- mentions
3460
+ mentions,
3461
+ softBreak
3273
3462
  }) {
3274
3463
  const mergedComponents = useMemo(
3275
3464
  () => ({ pre: CopyCodeBlock, ...components }),
@@ -3279,9 +3468,10 @@ function InkwellRenderer({
3279
3468
  () => parseMarkdown(content, {
3280
3469
  components: mergedComponents,
3281
3470
  rehypePlugins,
3282
- mentions
3471
+ mentions,
3472
+ softBreak
3283
3473
  }),
3284
- [content, mergedComponents, rehypePlugins, mentions]
3474
+ [content, mergedComponents, rehypePlugins, mentions, softBreak]
3285
3475
  );
3286
3476
  return /* @__PURE__ */ jsx("div", { className: `inkwell-renderer ${className ?? ""}`, children: rendered });
3287
3477
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@railway/inkwell",
3
- "version": "1.4.0",
3
+ "version": "2.1.0",
4
4
  "description": "Inkwell is a Markdown editor and renderer for React with an extensible plugin system.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -62,6 +62,7 @@
62
62
  "devDependencies": {
63
63
  "@testing-library/jest-dom": "^6.9.1",
64
64
  "@testing-library/react": "^16.3.2",
65
+ "@types/hast": "^3.0.4",
65
66
  "@types/mdast": "^4.0.4",
66
67
  "@types/node": "^24.0.0",
67
68
  "@types/react": "^19.0.0",
package/src/styles.css CHANGED
@@ -62,6 +62,34 @@
62
62
  "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo,
63
63
  Consolas, monospace;
64
64
  --inkwell-radius: 6px;
65
+
66
+ /* Typography & spacing. Defined once and consumed by both
67
+ `.inkwell-editor` and `.inkwell-renderer` so the two surfaces stay
68
+ WYSIWYG — what you type matches what renders. Override any token
69
+ on either surface to retune both (or use a per-surface selector
70
+ to retune just one). For chat-composer or compact embeds, set
71
+ `--inkwell-space-paragraph: 0` on the editor surface. */
72
+ --inkwell-font-size: 0.95rem;
73
+ --inkwell-line-height: 1.6;
74
+ --inkwell-heading-weight: 600;
75
+ --inkwell-heading-line-height: 1.3;
76
+ --inkwell-h1-size: 1.75em;
77
+ --inkwell-h2-size: 1.4em;
78
+ --inkwell-h3-size: 1.2em;
79
+ --inkwell-h4-size: 1em;
80
+ --inkwell-h5-size: 0.9em;
81
+ --inkwell-h6-size: 0.8em;
82
+ --inkwell-code-font-size: 0.85em;
83
+
84
+ --inkwell-space-paragraph: 0.5em;
85
+ --inkwell-space-heading: 0.75em;
86
+ --inkwell-space-blockquote: 1em;
87
+ --inkwell-space-list: 1em;
88
+ --inkwell-space-list-item: 0.25em;
89
+ --inkwell-list-indent: 1.5em;
90
+ --inkwell-space-code-block: 1em;
91
+ --inkwell-space-image: 1em;
92
+ --inkwell-space-hr: 2em;
65
93
  }
66
94
 
67
95
  @media (prefers-color-scheme: dark) {
@@ -118,8 +146,8 @@
118
146
  border-radius: var(--inkwell-radius);
119
147
  background: var(--inkwell-bg);
120
148
  color: var(--inkwell-text);
121
- line-height: 1.6;
122
- font-size: 0.95rem;
149
+ line-height: var(--inkwell-line-height);
150
+ font-size: var(--inkwell-font-size);
123
151
  transition: border-color 0.15s ease;
124
152
  }
125
153
  :where(.inkwell-editor:focus-within) {
@@ -127,8 +155,20 @@
127
155
  }
128
156
 
129
157
  /* `position: relative` on paragraphs is structural — Slate decorations and
130
- inline children position against it. Margin is theming and goes through
131
- `:where()` so a consumer paragraph-spacing utility wins. */
158
+ inline children position against it.
159
+
160
+ Margin stays at `0` here even though the renderer's paragraphs use
161
+ `--inkwell-space-paragraph`. Reason: the editor's content model emits
162
+ one `<p>` per source line, so a blank line in Markdown becomes an
163
+ empty `<p>` node between two paragraphs (a cursor target, kept for
164
+ round-trip fidelity). With a non-zero paragraph margin, those empty
165
+ paragraphs add their own top/bottom margin on top of the real
166
+ paragraphs' margins — visually multiplying the gap and breaking the
167
+ WYSIWYG promise in the other direction (editor looks more airy than
168
+ the renderer). Until the empty-paragraph encoding is reworked, the
169
+ editor opts out of the shared paragraph-spacing token. Consumers
170
+ who want non-zero spacing in the editor can set the margin
171
+ themselves with a higher-specificity rule. */
132
172
  .inkwell-editor p {
133
173
  position: relative;
134
174
  }
@@ -152,42 +192,61 @@
152
192
  padding: 0.1em 0.35em;
153
193
  border-radius: 4px;
154
194
  font-family: var(--inkwell-font-mono);
155
- font-size: 0.85em;
195
+ font-size: var(--inkwell-code-font-size);
196
+ }
197
+ /* Visible link text — mirrors `.inkwell-renderer a` so the editor stays
198
+ WYSIWYG. Applies to both the label inside `[text](url)` and the entire
199
+ text of a bare URL autolink. */
200
+ :where(.inkwell-editor-link) {
201
+ color: var(--inkwell-accent);
202
+ text-decoration: underline;
203
+ text-underline-offset: 2px;
204
+ }
205
+ /* URL token inside `(...)` of a markdown link. Inherits the dim color
206
+ from the `.inkwell-editor-marker` class it ships alongside; this rule
207
+ exists so consumers have a stable hook to restyle the URL separately
208
+ from generic markers (different color, hover state, etc.) without
209
+ touching every dimmed bracket / asterisk in the editor. Empty by
210
+ design — overriding `text-decoration: underline` etc. is a consumer
211
+ call. */
212
+ :where(.inkwell-editor-link-url) {
213
+ text-decoration: none;
156
214
  }
157
215
 
158
216
  :where(.inkwell-editor-blockquote) {
159
217
  border-left: 3px solid var(--inkwell-border-strong);
160
218
  padding-left: 0.85em;
161
- margin: 0.5em 0;
219
+ margin: var(--inkwell-space-blockquote) 0;
162
220
  color: var(--inkwell-text-muted);
163
221
  }
164
222
 
165
223
  :where(.inkwell-editor-heading) {
166
- font-weight: 600;
167
- line-height: 1.3;
224
+ font-weight: var(--inkwell-heading-weight);
225
+ line-height: var(--inkwell-heading-line-height);
226
+ margin: var(--inkwell-space-heading) 0;
168
227
  color: var(--inkwell-text);
169
228
  }
170
229
  :where(.inkwell-editor-heading-1) {
171
- font-size: 1.75em;
230
+ font-size: var(--inkwell-h1-size);
172
231
  }
173
232
  :where(.inkwell-editor-heading-2) {
174
- font-size: 1.4em;
233
+ font-size: var(--inkwell-h2-size);
175
234
  }
176
235
  :where(.inkwell-editor-heading-3) {
177
- font-size: 1.2em;
236
+ font-size: var(--inkwell-h3-size);
178
237
  }
179
238
  :where(.inkwell-editor-heading-4) {
180
- font-size: 1em;
239
+ font-size: var(--inkwell-h4-size);
181
240
  }
182
241
  :where(.inkwell-editor-heading-5) {
183
- font-size: 0.9em;
242
+ font-size: var(--inkwell-h5-size);
184
243
  }
185
244
  :where(.inkwell-editor-heading-6) {
186
- font-size: 0.8em;
245
+ font-size: var(--inkwell-h6-size);
187
246
  }
188
247
 
189
248
  :where(.inkwell-editor-image) {
190
- margin: 0.75em 0;
249
+ margin: var(--inkwell-space-image) 0;
191
250
  border-radius: var(--inkwell-radius);
192
251
  overflow: hidden;
193
252
  border: 1px solid transparent;
@@ -257,7 +316,7 @@
257
316
  :where(.inkwell-editor .inkwell-editor-code-fence),
258
317
  :where(.inkwell-renderer pre code) {
259
318
  font-family: var(--inkwell-font-mono);
260
- font-size: 0.85em;
319
+ font-size: var(--inkwell-code-font-size);
261
320
  line-height: 1.55;
262
321
  }
263
322
  /* Wrapping behavior for code lines stays structural — Slate emits one
@@ -442,40 +501,61 @@
442
501
  `!important`. */
443
502
  :where(.inkwell-renderer) {
444
503
  color: var(--inkwell-text);
445
- line-height: 1.65;
446
- font-size: 0.95rem;
504
+ line-height: var(--inkwell-line-height);
505
+ font-size: var(--inkwell-font-size);
447
506
  }
448
507
  :where(.inkwell-renderer :first-child) {
449
508
  margin-top: 0;
450
509
  }
451
510
  :where(.inkwell-renderer h1) {
452
- font-size: 1.75em;
453
- font-weight: 600;
454
- margin: 0.67em 0;
511
+ font-size: var(--inkwell-h1-size);
512
+ font-weight: var(--inkwell-heading-weight);
513
+ line-height: var(--inkwell-heading-line-height);
514
+ margin: var(--inkwell-space-heading) 0;
455
515
  }
456
516
  :where(.inkwell-renderer h2) {
457
- font-size: 1.4em;
458
- font-weight: 600;
459
- margin: 0.75em 0;
517
+ font-size: var(--inkwell-h2-size);
518
+ font-weight: var(--inkwell-heading-weight);
519
+ line-height: var(--inkwell-heading-line-height);
520
+ margin: var(--inkwell-space-heading) 0;
460
521
  }
461
522
  :where(.inkwell-renderer h3) {
462
- font-size: 1.2em;
463
- font-weight: 600;
464
- margin: 0.8em 0;
523
+ font-size: var(--inkwell-h3-size);
524
+ font-weight: var(--inkwell-heading-weight);
525
+ line-height: var(--inkwell-heading-line-height);
526
+ margin: var(--inkwell-space-heading) 0;
527
+ }
528
+ :where(.inkwell-renderer h4) {
529
+ font-size: var(--inkwell-h4-size);
530
+ font-weight: var(--inkwell-heading-weight);
531
+ line-height: var(--inkwell-heading-line-height);
532
+ margin: var(--inkwell-space-heading) 0;
533
+ }
534
+ :where(.inkwell-renderer h5) {
535
+ font-size: var(--inkwell-h5-size);
536
+ font-weight: var(--inkwell-heading-weight);
537
+ line-height: var(--inkwell-heading-line-height);
538
+ margin: var(--inkwell-space-heading) 0;
539
+ }
540
+ :where(.inkwell-renderer h6) {
541
+ font-size: var(--inkwell-h6-size);
542
+ font-weight: var(--inkwell-heading-weight);
543
+ line-height: var(--inkwell-heading-line-height);
544
+ margin: var(--inkwell-space-heading) 0;
465
545
  }
466
546
  :where(.inkwell-renderer p) {
467
- margin: 0.5em 0;
547
+ margin: var(--inkwell-space-paragraph) 0;
468
548
  }
469
549
  :where(.inkwell-renderer blockquote) {
470
550
  border-left: 3px solid var(--inkwell-border-strong);
471
551
  padding-left: 0.85em;
472
- margin: 1em 0;
552
+ margin: var(--inkwell-space-blockquote) 0;
473
553
  color: var(--inkwell-text-muted);
474
554
  }
475
555
  :where(.inkwell-renderer ul),
476
556
  :where(.inkwell-renderer ol) {
477
- padding-left: 1.5em;
478
- margin: 1em 0;
557
+ padding-left: var(--inkwell-list-indent);
558
+ margin: var(--inkwell-space-list) 0;
479
559
  }
480
560
  :where(.inkwell-renderer ul) {
481
561
  list-style: disc;
@@ -484,7 +564,7 @@
484
564
  list-style: decimal;
485
565
  }
486
566
  :where(.inkwell-renderer li) {
487
- margin: 0.25em 0;
567
+ margin: var(--inkwell-space-list-item) 0;
488
568
  }
489
569
  :where(.inkwell-renderer code) {
490
570
  background: var(--inkwell-code-bg);
@@ -492,7 +572,7 @@
492
572
  padding: 0.1em 0.35em;
493
573
  border-radius: 4px;
494
574
  font-family: var(--inkwell-font-mono);
495
- font-size: 0.85em;
575
+ font-size: var(--inkwell-code-font-size);
496
576
  }
497
577
  /* Code-block wrapper position is structural — the copy button absolutely
498
578
  positions inside it. */
@@ -533,7 +613,7 @@
533
613
  color: var(--inkwell-text);
534
614
  }
535
615
  :where(.inkwell-renderer pre) {
536
- margin: 1em 0;
616
+ margin: var(--inkwell-space-code-block) 0;
537
617
  border-radius: var(--inkwell-radius);
538
618
  overflow: auto;
539
619
  border: 1px solid var(--inkwell-border);
@@ -544,7 +624,6 @@
544
624
  padding: 0.85em 1em;
545
625
  background: transparent;
546
626
  color: var(--inkwell-text);
547
- font-size: 0.82em;
548
627
  }
549
628
  :where(.inkwell-renderer a) {
550
629
  color: var(--inkwell-accent);
@@ -554,7 +633,7 @@
554
633
  :where(.inkwell-renderer hr) {
555
634
  border: none;
556
635
  border-top: 1px solid var(--inkwell-border);
557
- margin: 2em 0;
636
+ margin: var(--inkwell-space-hr) 0;
558
637
  }
559
638
  :where(.inkwell-renderer strong) {
560
639
  font-weight: 600;
@@ -569,5 +648,5 @@
569
648
  max-width: 100%;
570
649
  height: auto;
571
650
  border-radius: var(--inkwell-radius);
572
- margin: 1em 0;
651
+ margin: var(--inkwell-space-image) 0;
573
652
  }