@railway/inkwell 2.0.0 → 2.1.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/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,8 +1242,16 @@ 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
- slate.Transforms.insertNodes(editor, nodes);
1254
+ slate.Transforms.insertFragment(editor, nodes);
1166
1255
  return;
1167
1256
  }
1168
1257
  insertData(data);
@@ -3163,6 +3252,32 @@ 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
+ }
3166
3281
  function splitTextOnNewlines(text) {
3167
3282
  if (!text.value.includes("\n")) return [text];
3168
3283
  const parts = text.value.split("\n");
@@ -3309,6 +3424,7 @@ function createProcessor2(options = {}) {
3309
3424
  proc.use(plugin);
3310
3425
  }
3311
3426
  }
3427
+ proc.use(rehypeTrimCodeBlockTrailingNewline);
3312
3428
  proc.use(rehypeSanitize__default.default, {
3313
3429
  ...rehypeSanitize.defaultSchema,
3314
3430
  tagNames: [...rehypeSanitize.defaultSchema.tagNames ?? [], "span"],
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,8 +1227,16 @@ 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
- Transforms.insertNodes(editor, nodes);
1239
+ Transforms.insertFragment(editor, nodes);
1151
1240
  return;
1152
1241
  }
1153
1242
  insertData(data);
@@ -3148,6 +3237,32 @@ 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
+ }
3151
3266
  function splitTextOnNewlines(text) {
3152
3267
  if (!text.value.includes("\n")) return [text];
3153
3268
  const parts = text.value.split("\n");
@@ -3294,6 +3409,7 @@ function createProcessor2(options = {}) {
3294
3409
  proc.use(plugin);
3295
3410
  }
3296
3411
  }
3412
+ proc.use(rehypeTrimCodeBlockTrailingNewline);
3297
3413
  proc.use(rehypeSanitize, {
3298
3414
  ...defaultSchema,
3299
3415
  tagNames: [...defaultSchema.tagNames ?? [], "span"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@railway/inkwell",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
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
@@ -194,6 +194,24 @@
194
194
  font-family: var(--inkwell-font-mono);
195
195
  font-size: var(--inkwell-code-font-size);
196
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;
214
+ }
197
215
 
198
216
  :where(.inkwell-editor-blockquote) {
199
217
  border-left: 3px solid var(--inkwell-border-strong);