@@ -19,7 +19,7 @@ import {
19
19
getSingletonHighlighter ,
20
20
Highlighter ,
21
21
} from "shiki" ;
22
- import { DiffLine } from ".." ;
22
+ import { DiffChar , DiffLine } from ".." ;
23
23
import { escapeForSVG , kebabOfThemeStr } from "../util/text" ;
24
24
25
25
interface CodeRendererOptions {
@@ -228,6 +228,7 @@ export class CodeRenderer {
228
228
options : ConversionOptions ,
229
229
currLineOffsetFromTop : number ,
230
230
newDiffLines : DiffLine [ ] ,
231
+ newDiffChars : DiffChar [ ] ,
231
232
) : Promise < Buffer > {
232
233
const strokeWidth = 1 ;
233
234
const highlightedCodeHtml = await this . highlightCode (
@@ -236,13 +237,14 @@ export class CodeRenderer {
236
237
currLineOffsetFromTop ,
237
238
newDiffLines ,
238
239
) ;
239
- // console.log(highlightedCodeHtml);
240
240
241
241
const { guts, lineBackgrounds } = this . convertShikiHtmlToSvgGut (
242
242
highlightedCodeHtml ,
243
243
options ,
244
+ newDiffChars ,
244
245
) ;
245
246
const backgroundColor = this . getBackgroundColor ( highlightedCodeHtml ) ;
247
+ const borderColor = "#6b6b6b" ;
246
248
247
249
const lines = code . split ( "\n" ) ;
248
250
const actualHeight = lines . length * options . lineHeight ;
@@ -256,24 +258,68 @@ export class CodeRenderer {
256
258
}
257
259
</style>
258
260
<g>
259
- <rect x="0" y="0" rx="10 " ry="10 " width="${ options . dimensions . width } " height="${ actualHeight } " fill="${ this . editorBackground } " shape-rendering="crispEdges" />
261
+ <rect x="0" y="0" rx="2 " ry="2 " width="${ options . dimensions . width } " height="${ actualHeight } " fill="${ backgroundColor } " stroke=" ${ borderColor } " stroke-width=" ${ strokeWidth } " shape-rendering="crispEdges" />
260
262
${ lineBackgrounds }
261
263
${ guts }
262
264
</g>
263
265
</svg>` ;
264
- // console.log(svg);
265
266
266
267
return Buffer . from ( svg , "utf8" ) ;
267
268
}
268
269
269
270
convertShikiHtmlToSvgGut (
270
271
shikiHtml : string ,
271
272
options : ConversionOptions ,
273
+ diffChars : DiffChar [ ] ,
272
274
) : { guts : string ; lineBackgrounds : string } {
273
275
const dom = new JSDOM ( shikiHtml ) ;
274
276
const document = dom . window . document ;
275
277
276
278
const lines = Array . from ( document . querySelectorAll ( ".line" ) ) ;
279
+
280
+ const additionSegmentsByLine = new Map <
281
+ number ,
282
+ Array < { start : number ; end : number } >
283
+ > ( ) ;
284
+
285
+ diffChars . forEach ( ( diff ) => {
286
+ if (
287
+ diff . type !== "new" ||
288
+ diff . newLineIndex === undefined ||
289
+ diff . newCharIndexInLine === undefined
290
+ ) {
291
+ return ;
292
+ }
293
+
294
+ if ( diff . char . includes ( "\n" ) ) {
295
+ return ;
296
+ }
297
+
298
+ const start = diff . newCharIndexInLine ;
299
+ const end = start + diff . char . length ;
300
+ const existing = additionSegmentsByLine . get ( diff . newLineIndex ) ?? [ ] ;
301
+ existing . push ( { start, end } ) ;
302
+ additionSegmentsByLine . set ( diff . newLineIndex , existing ) ;
303
+ } ) ;
304
+
305
+ additionSegmentsByLine . forEach ( ( segments , lineIndex ) => {
306
+ segments . sort ( ( a , b ) => a . start - b . start ) ;
307
+ const merged : Array < { start : number ; end : number } > = [ ] ;
308
+ segments . forEach ( ( segment ) => {
309
+ if ( merged . length === 0 ) {
310
+ merged . push ( { ...segment } ) ;
311
+ return ;
312
+ }
313
+
314
+ const last = merged [ merged . length - 1 ] ;
315
+ if ( segment . start <= last . end ) {
316
+ last . end = Math . max ( last . end , segment . end ) ;
317
+ } else {
318
+ merged . push ( { ...segment } ) ;
319
+ }
320
+ } ) ;
321
+ additionSegmentsByLine . set ( lineIndex , merged ) ;
322
+ } ) ;
277
323
const svgLines = lines . map ( ( line , index ) => {
278
324
const spans = Array . from ( line . childNodes )
279
325
. map ( ( node ) => {
@@ -309,60 +355,37 @@ export class CodeRenderer {
309
355
return `<text x="0" y="${ y } " font-family="${ options . fontFamily } " font-size="${ options . fontSize . toString ( ) } " xml:space="preserve" dominant-baseline="central" shape-rendering="crispEdges">${ spans } </text>` ;
310
356
} ) ;
311
357
358
+ const estimatedCharWidth = options . fontSize * 0.6 ;
359
+ const additionFill = "rgba(40, 167, 69, 0.25)" ;
360
+
312
361
const lineBackgrounds = lines
313
362
. map ( ( line , index ) => {
314
363
const classes = line ?. getAttribute ( "class" ) || "" ;
315
- const bgColor = classes . includes ( "highlighted" )
316
- ? this . editorLineHighlight
317
- : classes . includes ( "diff add" )
318
- ? "rgba(255, 255, 0, 0.2)"
319
- : this . editorBackground ;
320
-
321
364
const y = index * options . lineHeight ;
322
- const isFirst = index === 0 ;
323
- const isLast = index === lines . length - 1 ;
324
- const isSingleLine = isFirst && isLast ;
325
- const radius = 10 ;
326
-
327
- // Handle single line case (both first and last)
328
- if ( isSingleLine ) {
329
- return `<path d="M ${ radius } ${ y }
330
- L ${ options . dimensions . width - radius } ${ y }
331
- Q ${ options . dimensions . width } ${ y } ${ options . dimensions . width } ${ y + radius }
332
- L ${ options . dimensions . width } ${ y + options . lineHeight - radius }
333
- Q ${ options . dimensions . width } ${ y + options . lineHeight } ${ options . dimensions . width - radius } ${ y + options . lineHeight }
334
- L ${ radius } ${ y + options . lineHeight }
335
- Q ${ 0 } ${ y + options . lineHeight } ${ 0 } ${ y + options . lineHeight - radius }
336
- L ${ 0 } ${ y + radius }
337
- Q ${ 0 } ${ y } ${ radius } ${ y }
338
- Z"
339
- fill="${ bgColor } " />` ;
365
+ const segments = additionSegmentsByLine . get ( index ) ?? [ ] ;
366
+ const backgrounds : string [ ] = [ ] ;
367
+
368
+ if ( classes . includes ( "highlighted" ) ) {
369
+ backgrounds . push (
370
+ `<rect x="0" y="${ y } " width="100%" height="${ options . lineHeight } " fill="${ this . editorLineHighlight } " shape-rendering="crispEdges" />` ,
371
+ ) ;
340
372
}
341
373
342
- // SVG notes:
343
- // By default SVGs have anti-aliasing on.
344
- // This is undesirable in our case because pixel-perfect alignment of these rectangles will introduce thin gaps.
345
- // Turning it off with 'shape-rendering="crispEdges"' solves the issue.
346
- return isFirst
347
- ? `<path d="M ${ 0 } ${ y + options . lineHeight }
348
- L ${ 0 } ${ y + radius }
349
- Q ${ 0 } ${ y } ${ radius } ${ y }
350
- L ${ options . dimensions . width - radius } ${ y }
351
- Q ${ options . dimensions . width } ${ y } ${ options . dimensions . width } ${ y + radius }
352
- L ${ options . dimensions . width } ${ y + options . lineHeight }
353
- Z"
354
- fill="${ bgColor } " />`
355
- : isLast
356
- ? `<path d="M ${ 0 } ${ y }
357
- L ${ 0 } ${ y + options . lineHeight - radius }
358
- Q ${ 0 } ${ y + options . lineHeight } ${ radius } ${ y + options . lineHeight }
359
- L ${ options . dimensions . width - radius } ${ y + options . lineHeight }
360
- Q ${ options . dimensions . width } ${ y + options . lineHeight } ${ options . dimensions . width } ${ y + options . lineHeight - 10 }
361
- L ${ options . dimensions . width } ${ y }
362
- Z"
363
- fill="${ bgColor } " />`
364
- : `<rect x="0" y="${ y } " width="100%" height="${ options . lineHeight } " fill="${ bgColor } " shape-rendering="crispEdges" />` ;
374
+ segments . forEach ( ( { start, end } ) => {
375
+ const widthInChars = Math . max ( end - start , 0 ) ;
376
+ if ( widthInChars <= 0 ) {
377
+ return ;
378
+ }
379
+ const x = start * estimatedCharWidth ;
380
+ const segmentWidth = widthInChars * estimatedCharWidth ;
381
+ backgrounds . push (
382
+ `<rect x="${ x } " y="${ y } " width="${ segmentWidth } " height="${ options . lineHeight } " fill="${ additionFill } " shape-rendering="crispEdges" />` ,
383
+ ) ;
384
+ } ) ;
385
+
386
+ return backgrounds . join ( "\n" ) ;
365
387
} )
388
+ . filter ( ( bg ) => bg . length > 0 )
366
389
. join ( "\n" ) ;
367
390
368
391
return {
@@ -394,6 +417,7 @@ export class CodeRenderer {
394
417
options : ConversionOptions ,
395
418
currLineOffsetFromTop : number ,
396
419
newDiffLines : DiffLine [ ] ,
420
+ newDiffChars : DiffChar [ ] ,
397
421
) : Promise < DataUri > {
398
422
switch ( options . imageType ) {
399
423
// case "png":
@@ -413,6 +437,7 @@ export class CodeRenderer {
413
437
options ,
414
438
currLineOffsetFromTop ,
415
439
newDiffLines ,
440
+ newDiffChars ,
416
441
) ;
417
442
return `data:image/svg+xml;base64,${ svgBuffer . toString ( "base64" ) } ` ;
418
443
}
0 commit comments