Skip to content

Commit 4b356ab

Browse files
committed
feat(hyper-link): add regexp/match/handle options. (#525)
1 parent 8a14a69 commit 4b356ab

File tree

2 files changed

+88
-18
lines changed

2 files changed

+88
-18
lines changed

extensions/hyper-link/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Hyper link Extensions for CodeMirror6.
1414
npm install @uiw/codemirror-extensions-hyper-link --save
1515
```
1616

17+
```jsx
18+
import { hyperLink, hyperLinkExtension, hyperLinkStyle } from '@uiw/codemirror-extensions-hyper-link';
19+
```
20+
1721
## Usage
1822

1923
```jsx
@@ -46,6 +50,38 @@ const view = new EditorView({
4650
});
4751
```
4852

53+
Custom match content
54+
55+
```tsx
56+
import { EditorView } from '@codemirror/view';
57+
import { EditorState } from '@codemirror/state';
58+
import { hyperLinkExtension, hyperLinkStyle } from '@uiw/codemirror-extensions-hyper-link';
59+
60+
const code = `Hyper Link\n====`;
61+
62+
export const hyperLink: Extension = [
63+
hyperLinkExtension({
64+
regexp: /Hyper/gi,
65+
match: { Hyper: 'https://google.com' },
66+
handle: (value) => {
67+
if (value === 'Hyper') return 'https://google.com';
68+
return value;
69+
},
70+
}),
71+
hyperLinkStyle,
72+
];
73+
74+
const state = EditorState.create({
75+
doc: code,
76+
extensions: [hyperLink],
77+
});
78+
79+
const view = new EditorView({
80+
parent: document.querySelector('#editor'),
81+
state,
82+
});
83+
```
84+
4985
## Contributors
5086

5187
As always, thanks to our amazing contributors!

extensions/hyper-link/src/index.ts

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
1-
import { ViewPlugin, EditorView, Decoration, DecorationSet, WidgetType, ViewUpdate } from '@codemirror/view';
1+
import {
2+
ViewPlugin,
3+
EditorView,
4+
Decoration,
5+
DecorationSet,
6+
MatchDecorator,
7+
WidgetType,
8+
ViewUpdate,
9+
} from '@codemirror/view';
210
import { Extension, Range } from '@codemirror/state';
311
import { syntaxTree } from '@codemirror/language';
412

513
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
614

715
export interface HyperLinkState {
8-
from: number;
9-
to: number;
16+
at: number;
1017
url: string;
1118
}
1219

13-
class HyperLink extends WidgetType {
20+
class HyperLinkIcon extends WidgetType {
1421
private readonly state: HyperLinkState;
1522
constructor(state: HyperLinkState) {
1623
super();
1724
this.state = state;
1825
}
19-
eq(other: HyperLink) {
20-
return (
21-
this.state.url === other.state.url && this.state.to === other.state.to && this.state.from === other.state.from
22-
);
26+
eq(other: HyperLinkIcon) {
27+
return this.state.url === other.state.url && this.state.at === other.state.at;
2328
}
2429
toDOM() {
2530
const wrapper = document.createElement('a');
2631
wrapper.href = this.state.url;
27-
wrapper.target = '__blank';
32+
wrapper.target = '_blank';
2833
wrapper.innerHTML = pathStr;
2934
wrapper.className = 'cm-hyper-link-icon';
35+
wrapper.rel = 'nofollow';
3036
return wrapper;
3137
}
32-
ignoreEvent() {
33-
return false;
34-
}
3538
}
3639

3740
function hyperLinkDecorations(view: EditorView) {
@@ -44,9 +47,8 @@ function hyperLinkDecorations(view: EditorView) {
4447
const callExp: string = view.state.doc.sliceString(from, to);
4548
if (type.name === 'URL') {
4649
const widget = Decoration.widget({
47-
widget: new HyperLink({
48-
from,
49-
to,
50+
widget: new HyperLinkIcon({
51+
at: to,
5052
url: callExp,
5153
}),
5254
side: 1,
@@ -59,16 +61,48 @@ function hyperLinkDecorations(view: EditorView) {
5961
return Decoration.set(widgets);
6062
}
6163

62-
export function hyperLinkExtension() {
64+
const linkDecorator = (regexp?: RegExp, matchData?: Record<string, string>, matchFn?: (str: string) => string) =>
65+
new MatchDecorator({
66+
regexp: regexp || /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi,
67+
decorate: (add, from, to, match, view) => {
68+
const url = match[0];
69+
let urlStr = matchFn && typeof matchFn === 'function' ? matchFn(url) : url;
70+
if (matchData && matchData[url]) {
71+
urlStr = matchData[url];
72+
}
73+
const start = to,
74+
end = to;
75+
const linkIcon = new HyperLinkIcon({ at: start, url: urlStr });
76+
add(start, end, Decoration.widget({ widget: linkIcon, side: 1 }));
77+
},
78+
});
79+
80+
export type hyperLinkExtensionOptions = {
81+
regexp?: RegExp;
82+
match?: Record<string, string>;
83+
handle?: (value: string) => string;
84+
};
85+
86+
export function hyperLinkExtension({ regexp, match, handle }: hyperLinkExtensionOptions = {}) {
6387
return ViewPlugin.fromClass(
6488
class HyperLinkView {
89+
decorator?: MatchDecorator;
6590
decorations: DecorationSet;
6691
constructor(view: EditorView) {
67-
this.decorations = hyperLinkDecorations(view);
92+
if (regexp) {
93+
this.decorator = linkDecorator(regexp, match, handle);
94+
this.decorations = this.decorator.createDeco(view);
95+
} else {
96+
this.decorations = hyperLinkDecorations(view);
97+
}
6898
}
6999
update(update: ViewUpdate) {
70100
if (update.docChanged || update.viewportChanged) {
71-
this.decorations = hyperLinkDecorations(update.view);
101+
if (regexp && this.decorator) {
102+
this.decorations = this.decorator.updateDeco(update, this.decorations);
103+
} else {
104+
this.decorations = hyperLinkDecorations(update.view);
105+
}
72106
}
73107
}
74108
},

0 commit comments

Comments
 (0)