@@ -231,16 +231,100 @@ export function getInitials(name: JsonValue): string {
231231 return parts [ 0 ] . substring ( 0 , 2 ) . toUpperCase ( ) ;
232232}
233233
234+ /**
235+ * Best-effort removal of common Markdown syntax for short UI excerpts (not a full parser).
236+ * Ordered passes: fenced code, inline code, images/links/reference links, bare bracket labels,
237+ * strikethrough, emphasis, line-leading heading/blockquote/list/hr patterns, angle autolinks.
238+ */
239+ export function markdownSyntaxToPlainText ( raw : string ) : string {
240+ let s = String ( raw ) ;
241+
242+ // Fenced blocks → inner text with whitespace collapsed (keep readable snippet).
243+ s = s . replace ( / ` ` ` [ \w - ] * \n ? ( [ \s \S ] * ?) ` ` ` / g, ( _ , inner : string ) => {
244+ const t = inner . replace ( / \s + / g, ' ' ) . trim ( ) ;
245+ return t ? ` ${ t } ` : ' ' ;
246+ } ) ;
247+
248+ // Inline code `...`
249+ s = s . replace ( / ` ( [ ^ ` ] + ) ` / g, '$1' ) ;
250+
251+ // Images 
252+ s = s . replace ( / ! \[ ( [ ^ \] ] * ) \] \( [ ^ ) ] * \) / g, '$1' ) ;
253+
254+ // Links [text](url)
255+ s = s . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ] * \) / g, '$1' ) ;
256+
257+ // Reference-style [text][ref] or [text][]
258+ s = s . replace ( / \[ ( [ ^ \] ] + ) \] \[ [ ^ \] ] * \] / g, '$1' ) ;
259+
260+ // Remaining [label] (shortcut reference / bare brackets)
261+ s = s . replace ( / \[ ( [ ^ \] ] + ) \] / g, '$1' ) ;
262+
263+ // Strikethrough ~~...~~
264+ s = s . replace ( / ~ ~ ( [ ^ ~ ] + ) ~ ~ / g, '$1' ) ;
265+
266+ // Bold / italic (repeat to unwrap nested markers)
267+ for ( let i = 0 ; i < 12 ; i += 1 ) {
268+ const next = s
269+ . replace ( / \* \* ( [ ^ * ] + ) \* \* / g, '$1' )
270+ . replace ( / _ _ ( [ ^ _ ] + ) _ _ / g, '$1' )
271+ . replace ( / \* ( [ ^ * ] + ) \* / g, '$1' )
272+ . replace ( / _ ( [ ^ _ ] + ) _ / g, '$1' ) ;
273+ if ( next === s ) break ;
274+ s = next ;
275+ }
276+
277+ s = s
278+ . split ( '\n' )
279+ . map ( ( line ) => {
280+ let L = line . replace ( / ^ \s * # { 1 , 6 } \s + / , '' ) ;
281+ L = L . replace ( / ^ \s * > \s + / , '' ) ;
282+ L = L . replace ( / ^ \s * [ - * + ] \s + / , '' ) ;
283+ L = L . replace ( / ^ \s * \d + \. \s + / , '' ) ;
284+ if ( / ^ \s * ( \* { 3 , } | - { 3 , } | _ { 3 , } ) \s * $ / . test ( L ) ) return '' ;
285+ return L ;
286+ } )
287+ . join ( '\n' ) ;
288+
289+ // Autolinks <https://...> and <http://...>
290+ s = s . replace ( / < h t t p s ? : \/ \/ [ ^ > \s ] + > / gi, ( m ) => m . slice ( 1 , - 1 ) ) ;
291+
292+ return s ;
293+ }
294+
295+ /**
296+ * Join non-empty lines with ". " so card excerpts read like sentences; if a line already ends with
297+ * sentence punctuation (or trailing comma/semicolon/colon), join with a single space only.
298+ */
299+ export function lineBreaksToFullStops ( s : string ) : string {
300+ const lines = s . split ( / \r ? \n + / ) . map ( ( l ) => l . trim ( ) ) . filter ( Boolean ) ;
301+ if ( lines . length === 0 ) return '' ;
302+ if ( lines . length === 1 ) return lines [ 0 ] ;
303+ const weakEnd = ( t : string ) => {
304+ const u = t . trimEnd ( ) ;
305+ return / [ . ! ? … ] [ " ' ) \] ] ? \s * $ / . test ( u ) || / [ , ; : ] \s * $ / . test ( u ) ;
306+ } ;
307+ let out = lines [ 0 ] ;
308+ for ( let i = 1 ; i < lines . length ; i += 1 ) {
309+ const next = lines [ i ] ;
310+ out = out . trimEnd ( ) + ( weakEnd ( out ) ? ' ' : '. ' ) + next ;
311+ }
312+ return out ;
313+ }
314+
234315/**
235316 * Plain-text description snippet for graph node cards (no HTML).
236- * Normalizes whitespace; truncates to `maxChars` with an ellipsis when longer.
317+ * Strips common Markdown syntax, turns line breaks into sentence breaks where helpful, then normalizes
318+ * whitespace and truncates to `maxChars` with an ellipsis when longer.
237319 */
238320export function truncatePlainDescriptionForNodeCard (
239321 raw : string | null | undefined ,
240322 maxChars = 100
241323) : string {
242324 if ( raw == null || raw === '' ) return '' ;
243- const s = String ( raw ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
325+ const stripped = markdownSyntaxToPlainText ( String ( raw ) ) ;
326+ const joined = lineBreaksToFullStops ( stripped ) ;
327+ const s = joined . replace ( / \s + / g, ' ' ) . trim ( ) ;
244328 if ( ! s ) return '' ;
245329 if ( s . length <= maxChars ) return s ;
246330 return s . slice ( 0 , maxChars - 1 ) . trimEnd ( ) + '\u2026' ;
0 commit comments