testomatio-editor-blocks 0.4.78 → 0.4.80
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/package/editor/autoLink.d.ts +45 -0
- package/package/editor/autoLink.js +199 -0
- package/package/index.d.ts +2 -1
- package/package/index.js +2 -1
- package/package/styles.css +34 -0
- package/package.json +1 -1
- package/src/App.tsx +32 -6
- package/src/editor/autoLink.test.ts +111 -0
- package/src/editor/autoLink.ts +248 -0
- package/src/editor/styles.css +34 -0
- package/src/index.ts +8 -1
- /package/package/editor/{MentionAutocomplete.d.ts → useMentionAutocomplete.d.ts} +0 -0
- /package/package/editor/{MentionAutocomplete.js → useMentionAutocomplete.js} +0 -0
- /package/src/editor/{MentionAutocomplete.tsx → useMentionAutocomplete.tsx} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BlockNoteExtension } from "@blocknote/core";
|
|
2
|
+
export interface LinkMatch {
|
|
3
|
+
/** Offset of the first character of the URL within the scanned string. */
|
|
4
|
+
start: number;
|
|
5
|
+
/** Offset just past the last character of the URL. */
|
|
6
|
+
end: number;
|
|
7
|
+
/** The matched URL, used verbatim as the `href`. */
|
|
8
|
+
href: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Find every `http(s)://` URL inside a plain string and return their offsets.
|
|
12
|
+
* Pure and DOM-free so it can be unit-tested directly.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectLinks(text: string): LinkMatch[];
|
|
15
|
+
interface AutoLinkPluginOptions {
|
|
16
|
+
/**
|
|
17
|
+
* When `true`, a plain click opens the link. When `false` (default), the
|
|
18
|
+
* platform modifier (Cmd/Ctrl) is required so plain clicks still place the
|
|
19
|
+
* text cursor for editing — the safer default inside an editor.
|
|
20
|
+
*/
|
|
21
|
+
openOnPlainClick: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* BlockNote extension that renders bare `http(s)://` URLs as clickable links.
|
|
25
|
+
*
|
|
26
|
+
* Editor extensions are supplied at editor-creation time and cannot be carried
|
|
27
|
+
* by the schema, so consumers add this to their `useCreateBlockNote` call:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* useCreateBlockNote({
|
|
31
|
+
* schema: customSchema,
|
|
32
|
+
* extensions: [autoLinkExtension()],
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* By default a link opens on Cmd/Ctrl+click (plain clicks still edit the text).
|
|
37
|
+
* Pass `{ openOnPlainClick: true }` to open on a plain click instead.
|
|
38
|
+
*/
|
|
39
|
+
export declare class AutoLinkExtension extends BlockNoteExtension {
|
|
40
|
+
static key(): string;
|
|
41
|
+
constructor(options?: Partial<AutoLinkPluginOptions>);
|
|
42
|
+
}
|
|
43
|
+
/** Factory for the `extensions` option of `useCreateBlockNote`. */
|
|
44
|
+
export declare const autoLinkExtension: (options?: Partial<AutoLinkPluginOptions>) => AutoLinkExtension;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { BlockNoteExtension } from "@blocknote/core";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
4
|
+
/**
|
|
5
|
+
* Auto-linking paints bare `http(s)://…` URLs as clickable links *without*
|
|
6
|
+
* modifying the document. Like the tag-badge decoration, the underlying text is
|
|
7
|
+
* left untouched, so markdown serialization round-trips unchanged — a bare URL
|
|
8
|
+
* stays a bare URL (which GFM auto-links anyway) instead of being rewritten into
|
|
9
|
+
* `[url](url)`.
|
|
10
|
+
*
|
|
11
|
+
* A URL is greedily matched as `http://`/`https://` followed by any run of
|
|
12
|
+
* non-whitespace, non-angle-bracket, non-quote characters; trailing sentence
|
|
13
|
+
* punctuation and unbalanced closing brackets are then trimmed so that natural
|
|
14
|
+
* prose like `see https://example.com.` or `(https://example.com)` links the URL
|
|
15
|
+
* without swallowing the surrounding punctuation.
|
|
16
|
+
*/
|
|
17
|
+
const URL_DETECT_REGEXP = /https?:\/\/[^\s<>"'`]+/gi;
|
|
18
|
+
/** Trailing characters that are almost always prose punctuation, not URL. */
|
|
19
|
+
const TRAILING_PUNCTUATION = /[.,;:!?'"”’»)\]}]$/;
|
|
20
|
+
const CLOSING_TO_OPENING = {
|
|
21
|
+
")": "(",
|
|
22
|
+
"]": "[",
|
|
23
|
+
"}": "{",
|
|
24
|
+
};
|
|
25
|
+
function countChar(text, char) {
|
|
26
|
+
let count = 0;
|
|
27
|
+
for (const c of text) {
|
|
28
|
+
if (c === char)
|
|
29
|
+
count++;
|
|
30
|
+
}
|
|
31
|
+
return count;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Strip trailing punctuation that the greedy match may have swallowed. A closing
|
|
35
|
+
* bracket is only stripped when it is *unbalanced* (no matching opener inside the
|
|
36
|
+
* URL), so real URLs such as a Wikipedia `..._(disambiguation)` link keep their
|
|
37
|
+
* parentheses.
|
|
38
|
+
*/
|
|
39
|
+
function trimTrailingPunctuation(url) {
|
|
40
|
+
let result = url;
|
|
41
|
+
// Loop because stripping a bracket can expose more punctuation and vice versa,
|
|
42
|
+
// e.g. `https://x.com/a).` → `https://x.com/a)` → `https://x.com/a`.
|
|
43
|
+
for (;;) {
|
|
44
|
+
const last = result[result.length - 1];
|
|
45
|
+
if (last === undefined)
|
|
46
|
+
break;
|
|
47
|
+
const opening = CLOSING_TO_OPENING[last];
|
|
48
|
+
if (opening) {
|
|
49
|
+
if (countChar(result, last) > countChar(result, opening)) {
|
|
50
|
+
result = result.slice(0, -1);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (TRAILING_PUNCTUATION.test(last)) {
|
|
56
|
+
result = result.slice(0, -1);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Find every `http(s)://` URL inside a plain string and return their offsets.
|
|
65
|
+
* Pure and DOM-free so it can be unit-tested directly.
|
|
66
|
+
*/
|
|
67
|
+
export function detectLinks(text) {
|
|
68
|
+
const matches = [];
|
|
69
|
+
// Fresh regex per call so the shared `lastIndex` never leaks between calls.
|
|
70
|
+
const regexp = new RegExp(URL_DETECT_REGEXP.source, "gi");
|
|
71
|
+
let match;
|
|
72
|
+
while ((match = regexp.exec(text)) !== null) {
|
|
73
|
+
const href = trimTrailingPunctuation(match[0]);
|
|
74
|
+
// The trim only ever removes characters from the end, so `start` is stable.
|
|
75
|
+
if (href.length > 0) {
|
|
76
|
+
matches.push({ start: match.index, end: match.index + href.length, href });
|
|
77
|
+
}
|
|
78
|
+
// Defensive guard against a zero-length match looping forever (a URL always
|
|
79
|
+
// starts with `http`, so this should never trigger).
|
|
80
|
+
if (regexp.lastIndex === match.index) {
|
|
81
|
+
regexp.lastIndex++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return matches;
|
|
85
|
+
}
|
|
86
|
+
const autoLinkPluginKey = new PluginKey("testomatioAutoLink");
|
|
87
|
+
/**
|
|
88
|
+
* Text carrying a real `link` mark (already an anchor) or a `code` mark
|
|
89
|
+
* (inline code, where a URL is a literal, not a link) is skipped so we never
|
|
90
|
+
* double-decorate or linkify code.
|
|
91
|
+
*/
|
|
92
|
+
function hasBlockingMark(marks) {
|
|
93
|
+
return marks.some((mark) => mark.type.name === "link" || mark.type.name === "code");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build inline decorations for every URL in the document. Code blocks are
|
|
97
|
+
* skipped wholesale; inside every other block, each text node is scanned and any
|
|
98
|
+
* URL that isn't already a link or inline code is painted.
|
|
99
|
+
*/
|
|
100
|
+
function buildLinkDecorations(doc) {
|
|
101
|
+
const decorations = [];
|
|
102
|
+
doc.descendants((node, pos) => {
|
|
103
|
+
// Never linkify inside code blocks — a URL there is source text, not a link.
|
|
104
|
+
if (node.type.spec.code) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (!node.isText || !node.text || hasBlockingMark(node.marks)) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
for (const { start, end, href } of detectLinks(node.text)) {
|
|
111
|
+
decorations.push(Decoration.inline(pos + start, pos + end, {
|
|
112
|
+
class: "bn-auto-link",
|
|
113
|
+
"data-auto-href": href,
|
|
114
|
+
title: href,
|
|
115
|
+
},
|
|
116
|
+
// Stored on the decoration spec so the click handler can resolve the
|
|
117
|
+
// href from document coordinates without trusting the DOM.
|
|
118
|
+
{ autoHref: href }));
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
});
|
|
122
|
+
return DecorationSet.create(doc, decorations);
|
|
123
|
+
}
|
|
124
|
+
/** True when the pointer event carries the platform "open link" modifier. */
|
|
125
|
+
function hasOpenModifier(event) {
|
|
126
|
+
// Cmd on macOS, Ctrl elsewhere — the same convention as VS Code / IntelliJ.
|
|
127
|
+
return event.metaKey || event.ctrlKey;
|
|
128
|
+
}
|
|
129
|
+
function findAutoHrefAt(view, pos) {
|
|
130
|
+
const decorations = autoLinkPluginKey.getState(view.state);
|
|
131
|
+
if (!decorations)
|
|
132
|
+
return null;
|
|
133
|
+
const found = decorations.find(pos, pos);
|
|
134
|
+
for (const deco of found) {
|
|
135
|
+
const href = deco.spec.autoHref;
|
|
136
|
+
if (typeof href === "string")
|
|
137
|
+
return href;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function openHref(href) {
|
|
142
|
+
if (typeof window === "undefined")
|
|
143
|
+
return;
|
|
144
|
+
// `noopener,noreferrer` so the opened tab can't reach back via `window.opener`.
|
|
145
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
146
|
+
}
|
|
147
|
+
function autoLinkPlugin(options) {
|
|
148
|
+
return new Plugin({
|
|
149
|
+
key: autoLinkPluginKey,
|
|
150
|
+
state: {
|
|
151
|
+
init: (_config, state) => buildLinkDecorations(state.doc),
|
|
152
|
+
apply: (tr, value) => tr.docChanged ? buildLinkDecorations(tr.doc) : value,
|
|
153
|
+
},
|
|
154
|
+
props: {
|
|
155
|
+
decorations(state) {
|
|
156
|
+
return autoLinkPluginKey.getState(state);
|
|
157
|
+
},
|
|
158
|
+
handleClick(view, pos, event) {
|
|
159
|
+
if (!options.openOnPlainClick && !hasOpenModifier(event)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const href = findAutoHrefAt(view, pos);
|
|
163
|
+
if (!href)
|
|
164
|
+
return false;
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
openHref(href);
|
|
167
|
+
return true;
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* BlockNote extension that renders bare `http(s)://` URLs as clickable links.
|
|
174
|
+
*
|
|
175
|
+
* Editor extensions are supplied at editor-creation time and cannot be carried
|
|
176
|
+
* by the schema, so consumers add this to their `useCreateBlockNote` call:
|
|
177
|
+
*
|
|
178
|
+
* ```ts
|
|
179
|
+
* useCreateBlockNote({
|
|
180
|
+
* schema: customSchema,
|
|
181
|
+
* extensions: [autoLinkExtension()],
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* By default a link opens on Cmd/Ctrl+click (plain clicks still edit the text).
|
|
186
|
+
* Pass `{ openOnPlainClick: true }` to open on a plain click instead.
|
|
187
|
+
*/
|
|
188
|
+
export class AutoLinkExtension extends BlockNoteExtension {
|
|
189
|
+
static key() {
|
|
190
|
+
return "autoLink";
|
|
191
|
+
}
|
|
192
|
+
constructor(options = {}) {
|
|
193
|
+
var _a;
|
|
194
|
+
super();
|
|
195
|
+
this.addProsemirrorPlugin(autoLinkPlugin({ openOnPlainClick: (_a = options.openOnPlainClick) !== null && _a !== void 0 ? _a : false }));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Factory for the `extensions` option of `useCreateBlockNote`. */
|
|
199
|
+
export const autoLinkExtension = (options) => new AutoLinkExtension(options);
|
package/package/index.d.ts
CHANGED
|
@@ -5,12 +5,13 @@ export { testMetaBlock, addTestBlock } from "./editor/blocks/testMeta";
|
|
|
5
5
|
export { setMetaFieldSuggestions, getMetaFieldSuggestions, type MetaFieldSuggestion, type MetaFieldSuggestionsConfig, } from "./editor/testMetaFields";
|
|
6
6
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
7
7
|
export { tagBadgeExtension, TagBadgeExtension, TAGS_DETECT_REGEXP, detectTags, type TagMatch, } from "./editor/tagBadge";
|
|
8
|
+
export { autoLinkExtension, AutoLinkExtension, detectLinks, type LinkMatch, } from "./editor/autoLink";
|
|
8
9
|
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } from "./editor/customMarkdownConverter";
|
|
9
10
|
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
|
|
10
11
|
export { useStepImageUpload, setImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
|
|
11
12
|
export { setMentionSources, getMentionSources, parseActiveMention, resolveMentionSource, buildMentionInsertText, applyMention, filterMentionItems, resolveMentionQuery, normalizeMentionItems, parseMentionsFromJsonApi, type MentionSource, type MentionItem, type MentionItemsInput, type MentionSearchResult, type ActiveMention, type MentionJsonApiDocument, type MentionJsonApiResource, } from "./editor/mentionAutocomplete";
|
|
12
13
|
export { MentionMenu, type MentionMenuProps, } from "./editor/MentionMenu";
|
|
13
|
-
export { useMentionAutocomplete, type UseMentionAutocompleteOptions, type UseMentionAutocompleteResult, } from "./editor/
|
|
14
|
+
export { useMentionAutocomplete, type UseMentionAutocompleteOptions, type UseMentionAutocompleteResult, } from "./editor/useMentionAutocomplete";
|
|
14
15
|
export { setFileDisplayUrlResolver, resolveFileDisplayUrl, type FileDisplayUrlResolver, } from "./editor/fileDisplayUrl";
|
|
15
16
|
export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
|
|
16
17
|
export declare const testomatioEditorClassName = "markdown testomatio-editor";
|
package/package/index.js
CHANGED
|
@@ -5,12 +5,13 @@ export { testMetaBlock, addTestBlock } from "./editor/blocks/testMeta";
|
|
|
5
5
|
export { setMetaFieldSuggestions, getMetaFieldSuggestions, } from "./editor/testMetaFields";
|
|
6
6
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
7
7
|
export { tagBadgeExtension, TagBadgeExtension, TAGS_DETECT_REGEXP, detectTags, } from "./editor/tagBadge";
|
|
8
|
+
export { autoLinkExtension, AutoLinkExtension, detectLinks, } from "./editor/autoLink";
|
|
8
9
|
export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
|
|
9
10
|
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
|
|
10
11
|
export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
|
|
11
12
|
export { setMentionSources, getMentionSources, parseActiveMention, resolveMentionSource, buildMentionInsertText, applyMention, filterMentionItems, resolveMentionQuery, normalizeMentionItems, parseMentionsFromJsonApi, } from "./editor/mentionAutocomplete";
|
|
12
13
|
export { MentionMenu, } from "./editor/MentionMenu";
|
|
13
|
-
export { useMentionAutocomplete, } from "./editor/
|
|
14
|
+
export { useMentionAutocomplete, } from "./editor/useMentionAutocomplete";
|
|
14
15
|
export { setFileDisplayUrlResolver, resolveFileDisplayUrl, } from "./editor/fileDisplayUrl";
|
|
15
16
|
export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
|
|
16
17
|
export const testomatioEditorClassName = "markdown testomatio-editor";
|
package/package/styles.css
CHANGED
|
@@ -771,6 +771,26 @@ html.dark .testomatio-editor [data-content-type="heading"] .bn-tag-badge {
|
|
|
771
771
|
border-color: rgba(255, 255, 255, 0.12);
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
+
/* ============================================
|
|
775
|
+
AUTO-LINKED URLS
|
|
776
|
+
Bare `http(s)://` URLs are painted as links by a ProseMirror inline
|
|
777
|
+
decoration (see src/editor/autoLink.ts). The text stays editable and the
|
|
778
|
+
markdown is untouched; only the styling and a Cmd/Ctrl+click handler are
|
|
779
|
+
added. Opening on click is wired in the plugin, not via `pointer-events`.
|
|
780
|
+
============================================ */
|
|
781
|
+
.testomatio-editor .bn-auto-link {
|
|
782
|
+
color: var(--color-primary-700);
|
|
783
|
+
text-decoration: underline;
|
|
784
|
+
text-underline-offset: 2px;
|
|
785
|
+
cursor: pointer;
|
|
786
|
+
}
|
|
787
|
+
.testomatio-editor .bn-auto-link:hover {
|
|
788
|
+
color: var(--color-primary-600);
|
|
789
|
+
}
|
|
790
|
+
html.dark .testomatio-editor .bn-auto-link {
|
|
791
|
+
color: rgba(129, 140, 248, 1);
|
|
792
|
+
}
|
|
793
|
+
|
|
774
794
|
/* ============================================
|
|
775
795
|
TEST / SUITE METADATA BLOCK
|
|
776
796
|
Dimmed card for `<!-- test ... -->` / `<!-- suite ... -->` comments.
|
|
@@ -1932,6 +1952,20 @@ html.dark .bn-step-editor--preview a.step-preview-link {
|
|
|
1932
1952
|
font-size: var(--badge-fz-sm);
|
|
1933
1953
|
}
|
|
1934
1954
|
|
|
1955
|
+
.bn-mantine .bn-suggestion-menu {
|
|
1956
|
+
max-width: 24rem;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
.bn-mt-suggestion-menu-item-body {
|
|
1960
|
+
min-width: 0;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.bn-mt-suggestion-menu-item-subtitle {
|
|
1964
|
+
white-space: nowrap;
|
|
1965
|
+
overflow: hidden;
|
|
1966
|
+
text-overflow: ellipsis;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1935
1969
|
.bn-suggestion-icon {
|
|
1936
1970
|
display: inline-flex;
|
|
1937
1971
|
align-items: center;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
|
|
22
22
|
import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
23
23
|
import { tagBadgeExtension } from "./editor/tagBadge";
|
|
24
|
+
import { autoLinkExtension } from "./editor/autoLink";
|
|
24
25
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
25
26
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
26
27
|
import {
|
|
@@ -307,9 +308,24 @@ const DEMO_USERS: MentionItem[] = [
|
|
|
307
308
|
|
|
308
309
|
// Simulated "tests" API dataset — insertion uses the numeric id -> "@T{id}".
|
|
309
310
|
const DEMO_TESTS: MentionItem[] = [
|
|
310
|
-
{
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
{
|
|
312
|
+
id: "10231",
|
|
313
|
+
label: "Login redirects to dashboard",
|
|
314
|
+
detail:
|
|
315
|
+
"Given a registered user with valid credentials, when they submit the login form, the app authenticates against the API, persists the session token, and redirects to the dashboard within 2 seconds. Verifies the welcome banner shows the user's name and that the navigation reflects an authenticated state.",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: "10232",
|
|
319
|
+
label: "Login rejects wrong password",
|
|
320
|
+
detail:
|
|
321
|
+
"Given a registered user, when they submit the login form with an incorrect password, the app keeps them on the login page, shows an inline 'Invalid email or password' error, does not create a session, and increments the failed-attempt counter used for rate limiting after five consecutive failures.",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: "10245",
|
|
325
|
+
label: "Logout clears the session",
|
|
326
|
+
detail:
|
|
327
|
+
"Given an authenticated user on any page, when they trigger logout, the app revokes the session token server-side, clears local storage and cookies, redirects to the login screen, and ensures that pressing the browser back button does not restore access to protected routes.",
|
|
328
|
+
},
|
|
313
329
|
{ id: "10310", label: "Checkout applies discount code", detail: "#10310" },
|
|
314
330
|
{ id: "10311", label: "Checkout blocks empty cart", detail: "#10311" },
|
|
315
331
|
{ id: "10420", label: "Profile avatar upload", detail: "#10420" },
|
|
@@ -318,8 +334,18 @@ const DEMO_TESTS: MentionItem[] = [
|
|
|
318
334
|
];
|
|
319
335
|
|
|
320
336
|
const DEMO_SUITES: MentionItem[] = [
|
|
321
|
-
{
|
|
322
|
-
|
|
337
|
+
{
|
|
338
|
+
id: "1001",
|
|
339
|
+
label: "Authentication",
|
|
340
|
+
detail:
|
|
341
|
+
"End-to-end coverage of the authentication surface: login with valid and invalid credentials, session persistence and expiry, logout and token revocation, password reset via email, rate limiting after repeated failures, and redirect behaviour for protected routes when unauthenticated.",
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "1002",
|
|
345
|
+
label: "Checkout flow",
|
|
346
|
+
detail:
|
|
347
|
+
"Covers the full purchase journey from cart to confirmation: applying and rejecting discount codes, blocking checkout on an empty cart, tax and shipping calculation, payment authorization and failure handling, and the order confirmation screen with a valid receipt number.",
|
|
348
|
+
},
|
|
323
349
|
{ id: "1003", label: "User profile", detail: "#1003" },
|
|
324
350
|
{ id: "1004", label: "Notifications", detail: "#1004" },
|
|
325
351
|
{ id: "1005", label: "Admin panel", detail: "#1005" },
|
|
@@ -474,7 +500,7 @@ function CustomSlashMenu() {
|
|
|
474
500
|
function App() {
|
|
475
501
|
const editor = useCreateBlockNote({
|
|
476
502
|
schema: customSchema,
|
|
477
|
-
extensions: [tagBadgeExtension()],
|
|
503
|
+
extensions: [tagBadgeExtension(), autoLinkExtension()],
|
|
478
504
|
pasteHandler: createMarkdownPasteHandler(markdownToBlocks),
|
|
479
505
|
uploadFile: async (file: File) => {
|
|
480
506
|
const url = `https://placehold.co/600x400?text=${encodeURIComponent(file.name)}`;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { detectLinks } from "./autoLink";
|
|
3
|
+
|
|
4
|
+
describe("detectLinks", () => {
|
|
5
|
+
it("detects a single https URL", () => {
|
|
6
|
+
expect(detectLinks("https://example.com")).toEqual([
|
|
7
|
+
{ start: 0, end: 19, href: "https://example.com" },
|
|
8
|
+
]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("detects an http URL", () => {
|
|
12
|
+
expect(detectLinks("http://example.com")).toEqual([
|
|
13
|
+
{ start: 0, end: 18, href: "http://example.com" },
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("detects a URL embedded in prose", () => {
|
|
18
|
+
// "See https://example.com now" — the URL starts at index 4.
|
|
19
|
+
expect(detectLinks("See https://example.com now")).toEqual([
|
|
20
|
+
{ start: 4, end: 23, href: "https://example.com" },
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("detects multiple URLs in one string", () => {
|
|
25
|
+
const matches = detectLinks("a https://a.com b http://b.com");
|
|
26
|
+
expect(matches).toEqual([
|
|
27
|
+
{ start: 2, end: 15, href: "https://a.com" },
|
|
28
|
+
{ start: 18, end: 30, href: "http://b.com" },
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("keeps a full URL with path, query and fragment", () => {
|
|
33
|
+
const url = "https://example.com/path/to?q=1&x=2#frag";
|
|
34
|
+
expect(detectLinks(url)).toEqual([{ start: 0, end: url.length, href: url }]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("trims a trailing sentence period", () => {
|
|
38
|
+
expect(detectLinks("Read https://example.com.")).toEqual([
|
|
39
|
+
{ start: 5, end: 24, href: "https://example.com" },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("trims trailing punctuation like comma and question mark", () => {
|
|
44
|
+
expect(detectLinks("go to https://a.com, ok?")).toEqual([
|
|
45
|
+
{ start: 6, end: 19, href: "https://a.com" },
|
|
46
|
+
]);
|
|
47
|
+
expect(detectLinks("is it https://a.com?")).toEqual([
|
|
48
|
+
{ start: 6, end: 19, href: "https://a.com" },
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("excludes an unbalanced closing paren wrapping the URL", () => {
|
|
53
|
+
expect(detectLinks("(see https://example.com)")).toEqual([
|
|
54
|
+
{ start: 5, end: 24, href: "https://example.com" },
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps balanced parentheses that belong to the URL", () => {
|
|
59
|
+
const url = "https://en.wikipedia.org/wiki/Foo_(bar)";
|
|
60
|
+
expect(detectLinks(url)).toEqual([{ start: 0, end: url.length, href: url }]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("keeps a balanced paren but trims the trailing sentence period", () => {
|
|
64
|
+
const text = "https://en.wikipedia.org/wiki/Foo_(bar).";
|
|
65
|
+
expect(detectLinks(text)).toEqual([
|
|
66
|
+
{
|
|
67
|
+
start: 0,
|
|
68
|
+
end: text.length - 1,
|
|
69
|
+
href: "https://en.wikipedia.org/wiki/Foo_(bar)",
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("stops the URL at whitespace", () => {
|
|
75
|
+
expect(detectLinks("https://a.com and more")).toEqual([
|
|
76
|
+
{ start: 0, end: 13, href: "https://a.com" },
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not match bare domains without a scheme", () => {
|
|
81
|
+
expect(detectLinks("visit example.com or www.example.com")).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("does not match ftp or other schemes", () => {
|
|
85
|
+
expect(detectLinks("ftp://files.example.com")).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does not match a scheme with no host", () => {
|
|
89
|
+
expect(detectLinks("https://")).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns an empty array for text without URLs", () => {
|
|
93
|
+
expect(detectLinks("A plain sentence with no links")).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("is case-insensitive on the scheme", () => {
|
|
97
|
+
expect(detectLinks("HTTPS://Example.com")).toEqual([
|
|
98
|
+
{ start: 0, end: 19, href: "HTTPS://Example.com" },
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not keep stale regex state across calls", () => {
|
|
103
|
+
// Guards against a shared lastIndex leaking between invocations.
|
|
104
|
+
expect(detectLinks("https://one.com")).toEqual([
|
|
105
|
+
{ start: 0, end: 15, href: "https://one.com" },
|
|
106
|
+
]);
|
|
107
|
+
expect(detectLinks("https://two.com")).toEqual([
|
|
108
|
+
{ start: 0, end: 15, href: "https://two.com" },
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { BlockNoteExtension } from "@blocknote/core";
|
|
2
|
+
import type { Mark, Node as PMNode } from "prosemirror-model";
|
|
3
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
4
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
|
+
import type { EditorView } from "prosemirror-view";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Auto-linking paints bare `http(s)://…` URLs as clickable links *without*
|
|
9
|
+
* modifying the document. Like the tag-badge decoration, the underlying text is
|
|
10
|
+
* left untouched, so markdown serialization round-trips unchanged — a bare URL
|
|
11
|
+
* stays a bare URL (which GFM auto-links anyway) instead of being rewritten into
|
|
12
|
+
* `[url](url)`.
|
|
13
|
+
*
|
|
14
|
+
* A URL is greedily matched as `http://`/`https://` followed by any run of
|
|
15
|
+
* non-whitespace, non-angle-bracket, non-quote characters; trailing sentence
|
|
16
|
+
* punctuation and unbalanced closing brackets are then trimmed so that natural
|
|
17
|
+
* prose like `see https://example.com.` or `(https://example.com)` links the URL
|
|
18
|
+
* without swallowing the surrounding punctuation.
|
|
19
|
+
*/
|
|
20
|
+
const URL_DETECT_REGEXP = /https?:\/\/[^\s<>"'`]+/gi;
|
|
21
|
+
|
|
22
|
+
/** Trailing characters that are almost always prose punctuation, not URL. */
|
|
23
|
+
const TRAILING_PUNCTUATION = /[.,;:!?'"”’»)\]}]$/;
|
|
24
|
+
|
|
25
|
+
const CLOSING_TO_OPENING: Record<string, string> = {
|
|
26
|
+
")": "(",
|
|
27
|
+
"]": "[",
|
|
28
|
+
"}": "{",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface LinkMatch {
|
|
32
|
+
/** Offset of the first character of the URL within the scanned string. */
|
|
33
|
+
start: number;
|
|
34
|
+
/** Offset just past the last character of the URL. */
|
|
35
|
+
end: number;
|
|
36
|
+
/** The matched URL, used verbatim as the `href`. */
|
|
37
|
+
href: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function countChar(text: string, char: string): number {
|
|
41
|
+
let count = 0;
|
|
42
|
+
for (const c of text) {
|
|
43
|
+
if (c === char) count++;
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Strip trailing punctuation that the greedy match may have swallowed. A closing
|
|
50
|
+
* bracket is only stripped when it is *unbalanced* (no matching opener inside the
|
|
51
|
+
* URL), so real URLs such as a Wikipedia `..._(disambiguation)` link keep their
|
|
52
|
+
* parentheses.
|
|
53
|
+
*/
|
|
54
|
+
function trimTrailingPunctuation(url: string): string {
|
|
55
|
+
let result = url;
|
|
56
|
+
// Loop because stripping a bracket can expose more punctuation and vice versa,
|
|
57
|
+
// e.g. `https://x.com/a).` → `https://x.com/a)` → `https://x.com/a`.
|
|
58
|
+
for (;;) {
|
|
59
|
+
const last = result[result.length - 1];
|
|
60
|
+
if (last === undefined) break;
|
|
61
|
+
|
|
62
|
+
const opening = CLOSING_TO_OPENING[last];
|
|
63
|
+
if (opening) {
|
|
64
|
+
if (countChar(result, last) > countChar(result, opening)) {
|
|
65
|
+
result = result.slice(0, -1);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (TRAILING_PUNCTUATION.test(last)) {
|
|
72
|
+
result = result.slice(0, -1);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find every `http(s)://` URL inside a plain string and return their offsets.
|
|
83
|
+
* Pure and DOM-free so it can be unit-tested directly.
|
|
84
|
+
*/
|
|
85
|
+
export function detectLinks(text: string): LinkMatch[] {
|
|
86
|
+
const matches: LinkMatch[] = [];
|
|
87
|
+
// Fresh regex per call so the shared `lastIndex` never leaks between calls.
|
|
88
|
+
const regexp = new RegExp(URL_DETECT_REGEXP.source, "gi");
|
|
89
|
+
let match: RegExpExecArray | null;
|
|
90
|
+
while ((match = regexp.exec(text)) !== null) {
|
|
91
|
+
const href = trimTrailingPunctuation(match[0]);
|
|
92
|
+
// The trim only ever removes characters from the end, so `start` is stable.
|
|
93
|
+
if (href.length > 0) {
|
|
94
|
+
matches.push({ start: match.index, end: match.index + href.length, href });
|
|
95
|
+
}
|
|
96
|
+
// Defensive guard against a zero-length match looping forever (a URL always
|
|
97
|
+
// starts with `http`, so this should never trigger).
|
|
98
|
+
if (regexp.lastIndex === match.index) {
|
|
99
|
+
regexp.lastIndex++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return matches;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const autoLinkPluginKey = new PluginKey<DecorationSet>("testomatioAutoLink");
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Text carrying a real `link` mark (already an anchor) or a `code` mark
|
|
109
|
+
* (inline code, where a URL is a literal, not a link) is skipped so we never
|
|
110
|
+
* double-decorate or linkify code.
|
|
111
|
+
*/
|
|
112
|
+
function hasBlockingMark(marks: readonly Mark[]): boolean {
|
|
113
|
+
return marks.some(
|
|
114
|
+
(mark) => mark.type.name === "link" || mark.type.name === "code",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build inline decorations for every URL in the document. Code blocks are
|
|
120
|
+
* skipped wholesale; inside every other block, each text node is scanned and any
|
|
121
|
+
* URL that isn't already a link or inline code is painted.
|
|
122
|
+
*/
|
|
123
|
+
function buildLinkDecorations(doc: PMNode): DecorationSet {
|
|
124
|
+
const decorations: Decoration[] = [];
|
|
125
|
+
|
|
126
|
+
doc.descendants((node, pos) => {
|
|
127
|
+
// Never linkify inside code blocks — a URL there is source text, not a link.
|
|
128
|
+
if (node.type.spec.code) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!node.isText || !node.text || hasBlockingMark(node.marks)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const { start, end, href } of detectLinks(node.text)) {
|
|
137
|
+
decorations.push(
|
|
138
|
+
Decoration.inline(
|
|
139
|
+
pos + start,
|
|
140
|
+
pos + end,
|
|
141
|
+
{
|
|
142
|
+
class: "bn-auto-link",
|
|
143
|
+
"data-auto-href": href,
|
|
144
|
+
title: href,
|
|
145
|
+
},
|
|
146
|
+
// Stored on the decoration spec so the click handler can resolve the
|
|
147
|
+
// href from document coordinates without trusting the DOM.
|
|
148
|
+
{ autoHref: href },
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return undefined;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return DecorationSet.create(doc, decorations);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** True when the pointer event carries the platform "open link" modifier. */
|
|
160
|
+
function hasOpenModifier(event: MouseEvent): boolean {
|
|
161
|
+
// Cmd on macOS, Ctrl elsewhere — the same convention as VS Code / IntelliJ.
|
|
162
|
+
return event.metaKey || event.ctrlKey;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function findAutoHrefAt(view: EditorView, pos: number): string | null {
|
|
166
|
+
const decorations = autoLinkPluginKey.getState(view.state);
|
|
167
|
+
if (!decorations) return null;
|
|
168
|
+
const found = decorations.find(pos, pos);
|
|
169
|
+
for (const deco of found) {
|
|
170
|
+
const href = (deco.spec as { autoHref?: string }).autoHref;
|
|
171
|
+
if (typeof href === "string") return href;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function openHref(href: string): void {
|
|
177
|
+
if (typeof window === "undefined") return;
|
|
178
|
+
// `noopener,noreferrer` so the opened tab can't reach back via `window.opener`.
|
|
179
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface AutoLinkPluginOptions {
|
|
183
|
+
/**
|
|
184
|
+
* When `true`, a plain click opens the link. When `false` (default), the
|
|
185
|
+
* platform modifier (Cmd/Ctrl) is required so plain clicks still place the
|
|
186
|
+
* text cursor for editing — the safer default inside an editor.
|
|
187
|
+
*/
|
|
188
|
+
openOnPlainClick: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function autoLinkPlugin(options: AutoLinkPluginOptions): Plugin<DecorationSet> {
|
|
192
|
+
return new Plugin<DecorationSet>({
|
|
193
|
+
key: autoLinkPluginKey,
|
|
194
|
+
state: {
|
|
195
|
+
init: (_config, state) => buildLinkDecorations(state.doc),
|
|
196
|
+
apply: (tr, value) =>
|
|
197
|
+
tr.docChanged ? buildLinkDecorations(tr.doc) : value,
|
|
198
|
+
},
|
|
199
|
+
props: {
|
|
200
|
+
decorations(state) {
|
|
201
|
+
return autoLinkPluginKey.getState(state);
|
|
202
|
+
},
|
|
203
|
+
handleClick(view, pos, event) {
|
|
204
|
+
if (!options.openOnPlainClick && !hasOpenModifier(event)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const href = findAutoHrefAt(view, pos);
|
|
208
|
+
if (!href) return false;
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
openHref(href);
|
|
211
|
+
return true;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* BlockNote extension that renders bare `http(s)://` URLs as clickable links.
|
|
219
|
+
*
|
|
220
|
+
* Editor extensions are supplied at editor-creation time and cannot be carried
|
|
221
|
+
* by the schema, so consumers add this to their `useCreateBlockNote` call:
|
|
222
|
+
*
|
|
223
|
+
* ```ts
|
|
224
|
+
* useCreateBlockNote({
|
|
225
|
+
* schema: customSchema,
|
|
226
|
+
* extensions: [autoLinkExtension()],
|
|
227
|
+
* });
|
|
228
|
+
* ```
|
|
229
|
+
*
|
|
230
|
+
* By default a link opens on Cmd/Ctrl+click (plain clicks still edit the text).
|
|
231
|
+
* Pass `{ openOnPlainClick: true }` to open on a plain click instead.
|
|
232
|
+
*/
|
|
233
|
+
export class AutoLinkExtension extends BlockNoteExtension {
|
|
234
|
+
static key() {
|
|
235
|
+
return "autoLink";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
constructor(options: Partial<AutoLinkPluginOptions> = {}) {
|
|
239
|
+
super();
|
|
240
|
+
this.addProsemirrorPlugin(
|
|
241
|
+
autoLinkPlugin({ openOnPlainClick: options.openOnPlainClick ?? false }),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Factory for the `extensions` option of `useCreateBlockNote`. */
|
|
247
|
+
export const autoLinkExtension = (options?: Partial<AutoLinkPluginOptions>) =>
|
|
248
|
+
new AutoLinkExtension(options);
|
package/src/editor/styles.css
CHANGED
|
@@ -771,6 +771,26 @@ html.dark .testomatio-editor [data-content-type="heading"] .bn-tag-badge {
|
|
|
771
771
|
border-color: rgba(255, 255, 255, 0.12);
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
+
/* ============================================
|
|
775
|
+
AUTO-LINKED URLS
|
|
776
|
+
Bare `http(s)://` URLs are painted as links by a ProseMirror inline
|
|
777
|
+
decoration (see src/editor/autoLink.ts). The text stays editable and the
|
|
778
|
+
markdown is untouched; only the styling and a Cmd/Ctrl+click handler are
|
|
779
|
+
added. Opening on click is wired in the plugin, not via `pointer-events`.
|
|
780
|
+
============================================ */
|
|
781
|
+
.testomatio-editor .bn-auto-link {
|
|
782
|
+
color: var(--color-primary-700);
|
|
783
|
+
text-decoration: underline;
|
|
784
|
+
text-underline-offset: 2px;
|
|
785
|
+
cursor: pointer;
|
|
786
|
+
}
|
|
787
|
+
.testomatio-editor .bn-auto-link:hover {
|
|
788
|
+
color: var(--color-primary-600);
|
|
789
|
+
}
|
|
790
|
+
html.dark .testomatio-editor .bn-auto-link {
|
|
791
|
+
color: rgba(129, 140, 248, 1);
|
|
792
|
+
}
|
|
793
|
+
|
|
774
794
|
/* ============================================
|
|
775
795
|
TEST / SUITE METADATA BLOCK
|
|
776
796
|
Dimmed card for `<!-- test ... -->` / `<!-- suite ... -->` comments.
|
|
@@ -1932,6 +1952,20 @@ html.dark .bn-step-editor--preview a.step-preview-link {
|
|
|
1932
1952
|
font-size: var(--badge-fz-sm);
|
|
1933
1953
|
}
|
|
1934
1954
|
|
|
1955
|
+
.bn-mantine .bn-suggestion-menu {
|
|
1956
|
+
max-width: 24rem;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
.bn-mt-suggestion-menu-item-body {
|
|
1960
|
+
min-width: 0;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.bn-mt-suggestion-menu-item-subtitle {
|
|
1964
|
+
white-space: nowrap;
|
|
1965
|
+
overflow: hidden;
|
|
1966
|
+
text-overflow: ellipsis;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1935
1969
|
.bn-suggestion-icon {
|
|
1936
1970
|
display: inline-flex;
|
|
1937
1971
|
align-items: center;
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,13 @@ export {
|
|
|
23
23
|
type TagMatch,
|
|
24
24
|
} from "./editor/tagBadge";
|
|
25
25
|
|
|
26
|
+
export {
|
|
27
|
+
autoLinkExtension,
|
|
28
|
+
AutoLinkExtension,
|
|
29
|
+
detectLinks,
|
|
30
|
+
type LinkMatch,
|
|
31
|
+
} from "./editor/autoLink";
|
|
32
|
+
|
|
26
33
|
export {
|
|
27
34
|
blocksToMarkdown,
|
|
28
35
|
markdownToBlocks,
|
|
@@ -75,7 +82,7 @@ export {
|
|
|
75
82
|
useMentionAutocomplete,
|
|
76
83
|
type UseMentionAutocompleteOptions,
|
|
77
84
|
type UseMentionAutocompleteResult,
|
|
78
|
-
} from "./editor/
|
|
85
|
+
} from "./editor/useMentionAutocomplete";
|
|
79
86
|
|
|
80
87
|
export {
|
|
81
88
|
setFileDisplayUrlResolver,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|