@@ -6,14 +6,9 @@ import type { KernelMessage } from '@jupyterlab/services';
66import { parseScript } from 'meriyah' ;
77import { generate } from 'astring' ;
88
9- import { IMimeBundle } from './display ' ;
9+ import type { IMimeBundle } from '@jupyterlab/nbformat ' ;
1010
11- export {
12- IMimeBundle ,
13- IDisplayData ,
14- IDisplayCallbacks ,
15- DisplayHelper
16- } from './display' ;
11+ export { IDisplayData , IDisplayCallbacks , DisplayHelper } from './display' ;
1712
1813/**
1914 * Configuration for magic imports.
@@ -501,8 +496,10 @@ export class JavaScriptExecutor {
501496
502497 // Handle primitives
503498 if ( typeof value === 'string' ) {
504- // Check if it looks like HTML
505- if ( value . trim ( ) . startsWith ( '<' ) && value . trim ( ) . endsWith ( '>' ) ) {
499+ // Check if it looks like HTML (must start with a valid tag: <div>, <p class="...">,
500+ // <!DOCTYPE>, <!-- -->, <br/>, etc.). Rejects non-HTML like "<a, b>".
501+ const trimmed = value . trim ( ) ;
502+ if ( / ^ < (?: [ a - z A - Z ] [ a - z A - Z 0 - 9 - ] * [ \s \/ > ] | ! (?: D O C T Y P E | - - ) ) / . test ( trimmed ) && trimmed . endsWith ( '>' ) ) {
506503 return {
507504 'text/html' : value ,
508505 'text/plain' : value
@@ -746,23 +743,17 @@ export class JavaScriptExecutor {
746743
747744 const codeLine = lines [ lineIndex ] ;
748745
749- // Only match if cursor is at the end of the line
750- if ( cursorPosInLine !== codeLine . length ) {
751- return {
752- matches : [ ] ,
753- cursorStart : cursorPos ,
754- cursorEnd : cursorPos
755- } ;
756- }
757-
758- const lineRes = this . completeLine ( codeLine ) ;
746+ const codePrefix = codeLine . slice ( 0 , cursorPosInLine ) ;
747+ const lineRes = this . completeLine ( codePrefix ) ;
759748 const matches = lineRes . matches ;
760749 const inLineCursorStart = lineRes . cursorStart ;
750+ const tail = codeLine . slice ( cursorPosInLine ) ;
751+ const cursorTail = tail . match ( / ^ [ \w $ ] * / ) ?. [ 0 ] ?? '' ;
761752
762753 return {
763754 matches,
764755 cursorStart : lineBegin + inLineCursorStart ,
765- cursorEnd : cursorPos ,
756+ cursorEnd : cursorPos + cursorTail . length ,
766757 status : lineRes . status || 'ok'
767758 } ;
768759 }
@@ -1151,35 +1142,103 @@ export class JavaScriptExecutor {
11511142 * Transform import source with magic imports.
11521143 */
11531144 private _transformImportSource ( source : string ) : string {
1154- const noMagicStarts = [ 'http://' , 'https://' , 'data:' , 'file://' , 'blob:' ] ;
1155- const noEmsEnds = [ '.js' , '.mjs' , '.cjs' , '.wasm' , '+esm' ] ;
1156-
11571145 if ( ! this . _config . magicImports . enabled ) {
11581146 return source ;
11591147 }
11601148
1161- const baseUrl = this . _config . magicImports . baseUrl . endsWith ( '/' )
1162- ? this . _config . magicImports . baseUrl
1163- : this . _config . magicImports . baseUrl + '/' ;
1164-
1165- const addEms = ! noEmsEnds . some ( end => source . endsWith ( end ) ) ;
1166- const emsExtraEnd = addEms ? ( source . endsWith ( '/' ) ? '+esm' : '/+esm' ) : '' ;
1167-
1168- // If the source starts with http/https, don't transform
1169- if ( noMagicStarts . some ( start => source . startsWith ( start ) ) ) {
1149+ // Keep absolute, relative and import-map style specifiers unchanged.
1150+ if ( this . _isDirectImportSource ( source ) ) {
11701151 return source ;
11711152 }
11721153
1173- // If it starts with npm/ or gh/, or auto npm is disabled
1174- if (
1175- [ 'npm/' , 'gh/' ] . some ( start => source . startsWith ( start ) ) ||
1154+ const { path : sourcePath , suffix } = this . _splitImportSourceSuffix ( source ) ;
1155+
1156+ const transformedPath =
1157+ [ 'npm/' , 'gh/' ] . some ( start => sourcePath . startsWith ( start ) ) ||
11761158 ! this . _config . magicImports . enableAutoNpm
1177- ) {
1178- return `${ baseUrl } ${ source } ${ emsExtraEnd } ` ;
1159+ ? sourcePath
1160+ : `npm/${ sourcePath } ` ;
1161+
1162+ let transformedSource = `${ this . _joinBaseAndPath (
1163+ this . _config . magicImports . baseUrl ,
1164+ transformedPath
1165+ ) } ${ suffix } `;
1166+
1167+ if ( this . _shouldAppendEsmSuffix ( sourcePath ) ) {
1168+ transformedSource = this . _appendEsmSuffix ( transformedSource ) ;
1169+ }
1170+
1171+ return transformedSource ;
1172+ }
1173+
1174+ /**
1175+ * Whether an import source should bypass magic import transformation.
1176+ */
1177+ private _isDirectImportSource ( source : string ) : boolean {
1178+ return (
1179+ / ^ (?: [ a - z A - Z ] [ a - z A - Z \d + . - ] * : | \/ \/ ) / . test ( source ) ||
1180+ source . startsWith ( './' ) ||
1181+ source . startsWith ( '../' ) ||
1182+ source . startsWith ( '/' ) ||
1183+ source . startsWith ( '#' )
1184+ ) ;
1185+ }
1186+
1187+ /**
1188+ * Whether a transformed import should include the jsDelivr `+esm` suffix.
1189+ */
1190+ private _shouldAppendEsmSuffix ( sourcePath : string ) : boolean {
1191+ const noEsmEnds = [ '.js' , '.mjs' , '.cjs' , '.wasm' , '+esm' ] ;
1192+ return ! noEsmEnds . some ( end => sourcePath . endsWith ( end ) ) ;
1193+ }
1194+
1195+ /**
1196+ * Append `+esm` before query/hash suffixes.
1197+ */
1198+ private _appendEsmSuffix ( source : string ) : string {
1199+ const { path, suffix } = this . _splitImportSourceSuffix ( source ) ;
1200+ const esmSuffix = path . endsWith ( '/' ) ? '+esm' : '/+esm' ;
1201+ return `${ path } ${ esmSuffix } ${ suffix } ` ;
1202+ }
1203+
1204+ /**
1205+ * Split an import source into path and query/hash suffix.
1206+ */
1207+ private _splitImportSourceSuffix ( source : string ) : {
1208+ path : string ;
1209+ suffix : string ;
1210+ } {
1211+ const queryIndex = source . indexOf ( '?' ) ;
1212+ const hashIndex = source . indexOf ( '#' ) ;
1213+ const splitIndex =
1214+ queryIndex === - 1
1215+ ? hashIndex
1216+ : hashIndex === - 1
1217+ ? queryIndex
1218+ : Math . min ( queryIndex , hashIndex ) ;
1219+
1220+ if ( splitIndex === - 1 ) {
1221+ return { path : source , suffix : '' } ;
11791222 }
11801223
1181- // Auto-prefix with npm/
1182- return `${ baseUrl } npm/${ source } ${ emsExtraEnd } ` ;
1224+ return {
1225+ path : source . slice ( 0 , splitIndex ) ,
1226+ suffix : source . slice ( splitIndex )
1227+ } ;
1228+ }
1229+
1230+ /**
1231+ * Join a base URL and import path while preserving origin semantics.
1232+ */
1233+ private _joinBaseAndPath ( baseUrl : string , path : string ) : string {
1234+ const normalizedBase = baseUrl . endsWith ( '/' ) ? baseUrl : `${ baseUrl } /` ;
1235+ const normalizedPath = path . replace ( / ^ \/ + / , '' ) ;
1236+
1237+ try {
1238+ return new URL ( normalizedPath , normalizedBase ) . toString ( ) ;
1239+ } catch {
1240+ return `${ normalizedBase } ${ normalizedPath } ` ;
1241+ }
11831242 }
11841243
11851244 /**
@@ -1659,7 +1718,7 @@ export class JavaScriptExecutor {
16591718 expression : string ,
16601719 value : any ,
16611720 detailLevel : number
1662- ) : IMimeBundle {
1721+ ) : IInspectResult [ 'data' ] {
16631722 const lines : string [ ] = [ ] ;
16641723
16651724 // Type information
0 commit comments