@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 +118 -2
- package/dist/index.js +118 -2
- package/package.json +2 -1
- package/src/styles.css +18 -0
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.
|
|
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.
|
|
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.
|
|
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);
|