5
5
6
6
import { CancellationToken } from '../../../util/vs/base/common/cancellation' ;
7
7
import { CancellationError , isCancellationError } from '../../../util/vs/base/common/errors' ;
8
+ import { escapeRegExpCharacters } from '../../../util/vs/base/common/strings' ;
8
9
import { LinkifiedPart , LinkifiedText , coalesceParts } from './linkifiedText' ;
9
10
import type { IContributedLinkifier , ILinkifier , LinkifierContext } from './linkifyService' ;
10
11
11
12
namespace LinkifierState {
12
13
export enum Type {
13
14
Default ,
14
- CodeBlock ,
15
+ CodeOrMathBlock ,
15
16
Accumulating ,
16
17
}
17
18
18
19
export enum AccumulationType {
19
20
Word ,
20
- InlineCode ,
21
+ InlineCodeOrMath ,
21
22
PotentialLink ,
22
23
}
23
24
24
25
export const Default = { type : Type . Default } as const ;
25
26
26
- export class CodeBlock {
27
- readonly type = Type . CodeBlock ;
27
+ export class CodeOrMathBlock {
28
+ readonly type = Type . CodeOrMathBlock ;
28
29
29
30
constructor (
30
31
public readonly fence : string ,
31
32
public readonly indent : string ,
32
33
public readonly contents = '' ,
33
34
) { }
34
35
35
- appendContents ( text : string ) : CodeBlock {
36
- return new CodeBlock ( this . fence , this . indent , this . contents + text ) ;
36
+ appendContents ( text : string ) : CodeOrMathBlock {
37
+ return new CodeOrMathBlock ( this . fence , this . indent , this . contents + text ) ;
37
38
}
38
39
}
39
40
@@ -43,10 +44,15 @@ namespace LinkifierState {
43
44
constructor (
44
45
public readonly pendingText : string ,
45
46
public readonly accumulationType = LinkifierState . AccumulationType . Word ,
47
+ public readonly terminator ?: string ,
46
48
) { }
49
+
50
+ append ( text : string ) : Accumulating {
51
+ return new Accumulating ( this . pendingText + text , this . accumulationType , this . terminator ) ;
52
+ }
47
53
}
48
54
49
- export type State = typeof Default | CodeBlock | Accumulating ;
55
+ export type State = typeof Default | CodeOrMathBlock | Accumulating ;
50
56
}
51
57
52
58
/**
@@ -91,7 +97,21 @@ export class Linkifier implements ILinkifier {
91
97
92
98
// `text...
93
99
if ( / ^ [ ^ \[ ` ] * ` [ ^ ` ] * $ / . test ( part ) ) {
94
- this . _state = new LinkifierState . Accumulating ( part , LinkifierState . AccumulationType . InlineCode ) ;
100
+ this . _state = new LinkifierState . Accumulating ( part , LinkifierState . AccumulationType . InlineCodeOrMath , '`' ) ;
101
+ }
102
+ // `text`
103
+ else if ( / ^ ` [ ^ ` ] + ` $ / . test ( part ) ) {
104
+ // No linkifying inside inline code
105
+ out . push ( ...( await this . doLinkifyAndAppend ( part , { skipUnlikify : true } , token ) ) . parts ) ;
106
+ }
107
+ // $text...
108
+ else if ( / ^ [ ^ \[ ` ] * \$ [ ^ \$ ] * $ / . test ( part ) ) {
109
+ this . _state = new LinkifierState . Accumulating ( part , LinkifierState . AccumulationType . InlineCodeOrMath , '$' ) ;
110
+ }
111
+ // $text$
112
+ else if ( / ^ [ ^ \[ ` ] * \$ [ ^ \$ ] * \$ $ / . test ( part ) ) {
113
+ // No linkifying inside math code
114
+ out . push ( this . doAppend ( part ) ) ;
95
115
}
96
116
// [text...
97
117
else if ( / ^ \s * \[ [ ^ \] ] * $ / . test ( part ) ) {
@@ -104,10 +124,10 @@ export class Linkifier implements ILinkifier {
104
124
}
105
125
break ;
106
126
}
107
- case LinkifierState . Type . CodeBlock : {
127
+ case LinkifierState . Type . CodeOrMathBlock : {
108
128
if (
109
- new RegExp ( '(^|\\n)' + this . _state . fence + '($|\\n)' ) . test ( part )
110
- || ( this . _state . contents . length > 2 && new RegExp ( '(^|\\n)\\s*' + this . _state . fence + '($|\\n\\s*$)' ) . test ( this . _appliedText + part ) )
129
+ new RegExp ( '(^|\\n)' + escapeRegExpCharacters ( this . _state . fence ) + '($|\\n)' ) . test ( part )
130
+ || ( this . _state . contents . length > 2 && new RegExp ( '(^|\\n)\\s*' + escapeRegExpCharacters ( this . _state . fence ) + '($|\\n\\s*$)' ) . test ( this . _appliedText + part ) )
111
131
) {
112
132
// To end the code block, the previous text needs to be empty up the start of the last line and
113
133
// at lower indentation than the opening code block.
@@ -126,45 +146,66 @@ export class Linkifier implements ILinkifier {
126
146
break ;
127
147
}
128
148
case LinkifierState . Type . Accumulating : {
129
- const completeWord = async ( state : LinkifierState . Accumulating ) => {
130
- const toAppend = state . pendingText + part ;
149
+ const completeWord = async ( state : LinkifierState . Accumulating , inPart : string , skipUnlikify : boolean ) => {
150
+ const toAppend = state . pendingText + inPart ;
131
151
this . _state = LinkifierState . Default ;
132
- const r = await this . doLinkifyAndAppend ( toAppend , token ) ;
152
+ const r = await this . doLinkifyAndAppend ( toAppend , { skipUnlikify } , token ) ;
133
153
out . push ( ...r . parts ) ;
134
154
} ;
135
155
136
156
if ( this . _state . accumulationType === LinkifierState . AccumulationType . PotentialLink ) {
137
157
if ( / ] / . test ( part ) ) {
138
- this . _state = new LinkifierState . Accumulating ( this . _state . pendingText + part , LinkifierState . AccumulationType . Word ) ;
158
+ this . _state = this . _state . append ( part ) ;
139
159
break ;
140
160
} else if ( / \n / . test ( part ) ) {
141
- await completeWord ( this . _state ) ;
161
+ await completeWord ( this . _state , part , false ) ;
142
162
break ;
143
163
}
144
- } else if ( this . _state . accumulationType === LinkifierState . AccumulationType . InlineCode && / ` / . test ( part ) ) {
145
- await completeWord ( this . _state ) ;
164
+ } else if ( this . _state . accumulationType === LinkifierState . AccumulationType . InlineCodeOrMath && new RegExp ( escapeRegExpCharacters ( this . _state . terminator ?? '`' ) ) . test ( part ) ) {
165
+ const terminator = this . _state . terminator ?? '`' ;
166
+ const terminalIndex = part . indexOf ( terminator ) ;
167
+ if ( terminalIndex === - 1 ) {
168
+ await completeWord ( this . _state , part , true ) ;
169
+ } else {
170
+ if ( terminator === '`' ) {
171
+ await completeWord ( this . _state , part , true ) ;
172
+ } else {
173
+ // Math shouldn't run linkifies
174
+
175
+ const pre = part . slice ( 0 , terminalIndex + terminator . length ) ;
176
+ // No linkifying inside inline math
177
+ out . push ( this . doAppend ( this . _state . pendingText + pre ) ) ;
178
+
179
+ // But we can linkify after
180
+ const rest = part . slice ( terminalIndex + terminator . length ) ;
181
+ this . _state = LinkifierState . Default ;
182
+ if ( rest . length ) {
183
+ out . push ( ...( await this . doLinkifyAndAppend ( rest , { skipUnlikify : true } , token ) ) . parts ) ;
184
+ }
185
+ }
186
+ }
146
187
break ;
147
188
} else if ( this . _state . accumulationType === LinkifierState . AccumulationType . Word && / \s / . test ( part ) ) {
148
189
const toAppend = this . _state . pendingText + part ;
149
190
this . _state = LinkifierState . Default ;
150
191
151
192
// Check if we've found special tokens
152
- const fence = toAppend . match ( / ( ^ | \n ) \s * ( ` { 3 , } | ~ { 3 , } ) / ) ;
193
+ const fence = toAppend . match ( / ( ^ | \n ) \s * ( ` { 3 , } | ~ { 3 , } | \$ \$ ) / ) ;
153
194
if ( fence ) {
154
195
const indent = this . _appliedText . match ( / ( \n | ^ ) ( [ \t ] * ) $ / ) ;
155
- this . _state = new LinkifierState . CodeBlock ( fence [ 2 ] , indent ?. [ 2 ] ?? '' ) ;
196
+ this . _state = new LinkifierState . CodeOrMathBlock ( fence [ 2 ] , indent ?. [ 2 ] ?? '' ) ;
156
197
out . push ( this . doAppend ( toAppend ) ) ;
157
198
}
158
199
else {
159
- const r = await this . doLinkifyAndAppend ( toAppend , token ) ;
200
+ const r = await this . doLinkifyAndAppend ( toAppend , { } , token ) ;
160
201
out . push ( ...r . parts ) ;
161
202
}
162
203
163
204
break ;
164
205
}
165
206
166
207
// Keep accumulating
167
- this . _state = new LinkifierState . Accumulating ( this . _state . pendingText + part , this . _state . accumulationType ) ;
208
+ this . _state = this . _state . append ( part ) ;
168
209
break ;
169
210
}
170
211
}
@@ -176,13 +217,13 @@ export class Linkifier implements ILinkifier {
176
217
let out : LinkifiedText | undefined ;
177
218
178
219
switch ( this . _state . type ) {
179
- case LinkifierState . Type . CodeBlock : {
220
+ case LinkifierState . Type . CodeOrMathBlock : {
180
221
out = { parts : [ this . doAppend ( this . _state . contents ) ] } ;
181
222
break ;
182
223
}
183
224
case LinkifierState . Type . Accumulating : {
184
225
const toAppend = this . _state . pendingText ;
185
- out = await this . doLinkifyAndAppend ( toAppend , token ) ;
226
+ out = await this . doLinkifyAndAppend ( toAppend , { } , token ) ;
186
227
break ;
187
228
}
188
229
}
@@ -196,7 +237,11 @@ export class Linkifier implements ILinkifier {
196
237
return newText ;
197
238
}
198
239
199
- private async doLinkifyAndAppend ( newText : string , token : CancellationToken ) : Promise < LinkifiedText > {
240
+ private async doLinkifyAndAppend ( newText : string , options : { skipUnlikify ?: boolean } , token : CancellationToken ) : Promise < LinkifiedText > {
241
+ if ( newText . length === 0 ) {
242
+ return { parts : [ ] } ;
243
+ }
244
+
200
245
this . doAppend ( newText ) ;
201
246
202
247
// Run contributed linkifiers
@@ -210,19 +255,21 @@ export class Linkifier implements ILinkifier {
210
255
211
256
// Do a final pass that un-linkifies any file links that don't have a scheme.
212
257
// This prevents links like: [some text](index.html) from sneaking through as these can never be opened properly.
213
- parts = parts . map ( part => {
214
- if ( typeof part === 'string' ) {
215
- return part . replaceAll ( / \[ ( [ ^ \[ \] ] + ) \] \( ( [ ^ \s \) ] + ) \) / g, ( matched , text , path ) => {
216
- // Always preserve product URI scheme links
217
- if ( path . startsWith ( this . productUriScheme + ':' ) ) {
218
- return matched ;
219
- }
258
+ if ( ! options . skipUnlikify ) {
259
+ parts = parts . map ( part => {
260
+ if ( typeof part === 'string' ) {
261
+ return part . replaceAll ( / \[ ( [ ^ \[ \] ] + ) \] \( ( [ ^ \s \) ] + ) \) / g, ( matched , text , path ) => {
262
+ // Always preserve product URI scheme links
263
+ if ( path . startsWith ( this . productUriScheme + ':' ) ) {
264
+ return matched ;
265
+ }
220
266
221
- return / ^ \w + : / . test ( path ) ? matched : text ;
222
- } ) ;
223
- }
224
- return part ;
225
- } ) ;
267
+ return / ^ \w + : / . test ( path ) ? matched : text ;
268
+ } ) ;
269
+ }
270
+ return part ;
271
+ } ) ;
272
+ }
226
273
227
274
this . _totalAddedLinkCount += parts . filter ( part => typeof part !== 'string' ) . length ;
228
275
return { parts } ;
0 commit comments