@@ -109,20 +109,54 @@ function normalizeMarkdownImageTarget(rawTarget: string): string {
109109 return trimmed ;
110110}
111111
112- function normalizeAssetReference ( rawRef : string ) : string | null {
112+ function normalizeAssetPathLikeReference ( rawRef : string ) : string {
113113 const trimmed = rawRef . trim ( ) ;
114- if ( ! trimmed ) return null ;
114+ if ( ! trimmed ) return "" ;
115115
116116 const noQuotes = trimmed . replace ( / ^ [ " ' ] | [ " ' ] $ / g, "" ) ;
117117 const noAngles = noQuotes . replace ( / ^ < | > $ / g, "" ) ;
118118 const noQueryOrHash = noAngles . split ( / [ ? # ] / , 1 ) [ 0 ] ?. trim ( ) ?? "" ;
119119 const normalizedSlashes = noQueryOrHash . replace ( / \\ / g, "/" ) . replace ( / \/ { 2 , } / g, "/" ) ;
120- const normalizedLeading = normalizedSlashes . replace ( / ^ \. \/ / , "" ) . replace ( / ^ \/ / , "" ) ;
120+ return normalizedSlashes . replace ( / ^ \. \/ / , "" ) . replace ( / ^ \/ / , "" ) ;
121+ }
122+
123+ function isDirectoryAssetReference ( path : string ) : boolean {
124+ return path === "assets" || path === "assets/" || ( path . startsWith ( "assets/" ) && path . endsWith ( "/" ) ) ;
125+ }
126+
127+ function normalizeAssetReference ( rawRef : string ) : string | null {
128+ const normalizedLeading = normalizeAssetPathLikeReference ( rawRef ) ;
121129 if ( ! normalizedLeading . startsWith ( "assets/" ) ) return null ;
130+ if ( isDirectoryAssetReference ( normalizedLeading ) ) return null ;
122131
123132 return normalizedLeading ;
124133}
125134
135+ export function extractInvalidDirectoryAssetReferences ( markdown : string ) : string [ ] {
136+ const invalidRefs = new Set < string > ( ) ;
137+ markdownImageRefRegex . lastIndex = 0 ;
138+ typstImageRefRegex . lastIndex = 0 ;
139+
140+ let match : RegExpExecArray | null ;
141+ while ( ( match = markdownImageRefRegex . exec ( markdown ) ) !== null ) {
142+ const candidate = match [ 1 ] ? normalizeMarkdownImageTarget ( match [ 1 ] ) : ( match [ 2 ] ?? "" ) ;
143+ const normalized = normalizeAssetPathLikeReference ( candidate ) ;
144+ if ( isDirectoryAssetReference ( normalized ) ) {
145+ invalidRefs . add ( normalized ) ;
146+ }
147+ }
148+
149+ while ( ( match = typstImageRefRegex . exec ( markdown ) ) !== null ) {
150+ const candidate = match [ 1 ] ?? match [ 2 ] ?? "" ;
151+ const normalized = normalizeAssetPathLikeReference ( candidate ) ;
152+ if ( isDirectoryAssetReference ( normalized ) ) {
153+ invalidRefs . add ( normalized ) ;
154+ }
155+ }
156+
157+ return Array . from ( invalidRefs ) . sort ( ) ;
158+ }
159+
126160export function extractReferencedAssetPaths ( markdown : string ) : string [ ] {
127161 const paths = new Set < string > ( ) ;
128162 markdownImageRefRegex . lastIndex = 0 ;
@@ -631,6 +665,13 @@ export async function renderMarkdownToPdf(
631665
632666 const options : Required < RenderOptions > = { ...DEFAULT_RENDER_OPTIONS , ...( rawOptions ?? { } ) } ;
633667 const normalizedMarkdown = normalizeMarkdownForPdf ( markdown ) ;
668+ const invalidDirectoryReferences = extractInvalidDirectoryAssetReferences ( normalizedMarkdown ) ;
669+ if ( invalidDirectoryReferences . length > 0 ) {
670+ const sample = invalidDirectoryReferences . slice ( 0 , 4 ) . join ( ", " ) ;
671+ throw new Error (
672+ `Invalid image path points to an assets directory: ${ sample } . Use a file path under assets/ (for example assets/image.png).` ,
673+ ) ;
674+ }
634675
635676 await ensureTmpDir ( ) ;
636677 const id = randomBytes ( 8 ) . toString ( "hex" ) ;
0 commit comments