@@ -142,6 +142,7 @@ import {
142142 ICON_DIR , ICON_FILE , ICON_REPO , ICON_BRANCH , ICON_TAG , ICON_ISSUE_OPEN , ICON_ISSUE_CLOSED ,
143143 ICON_PR_OPEN , ICON_PR_TAB , ICON_PR_MERGED , ICON_PR_CLOSED ,
144144} from './lib/helpers.js' ;
145+ import { renderMarkdown } from './lib/markdown.js' ;
145146
146147const execFileP = promisify ( execFile ) ;
147148
@@ -317,91 +318,8 @@ async function readJsonBody(request, cap = ISSUE_BODY_CAP + 8192) {
317318 } catch { return null ; }
318319}
319320
320- // ------------------------------------------------------ markdown (bounded)
321- // Grammar (deliberately small; escape-first, so it is structurally
322- // XSS-proof — raw text is HTML-escaped BEFORE any tag is introduced):
323- // blocks: # h1..###### h6 | ``` fenced code | > blockquote |
324- // -/* unordered list | 1. ordered list | paragraphs (blank-line
325- // separated). No nesting, no tables, no HTML passthrough.
326- // inline: `code` | **bold** | *em* / _em_ | [text](href) |
327- // . hrefs: http(s) or relative only (no other
328- // schemes, no scheme-relative //); relative images are routed
329- // through the raw endpoint, relative links through blob view.
330-
331- function safeHref ( url , base ) {
332- if ( / ^ h t t p s ? : \/ \/ / i. test ( url ) ) return url ;
333- if ( url . startsWith ( '#' ) ) return url ;
334- if ( url . startsWith ( '//' ) ) return null ;
335- if ( / ^ [ a - z ] [ a - z 0 - 9 + . - ] * : / i. test ( url ) ) return null ; // javascript:, data:, …
336- let rel = url . replace ( / ^ \. \/ / , '' ) ;
337- if ( rel . startsWith ( '/' ) || rel . split ( '/' ) . some ( ( s ) => s === '..' || s === '' ) ) return null ;
338- return `${ base } /${ rel } ` ;
339- }
340-
341- function mdInline ( escaped , { rawBase, blobBase } ) {
342- const codes = [ ] ;
343- let s = escaped . replace ( / ` ( [ ^ ` ] + ) ` / g, ( m , c ) => { codes . push ( c ) ; return `\x01${ codes . length - 1 } \x01` ; } ) ;
344- s = s . replace ( / ! \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) \s ] + ) \) / g, ( m , alt , url ) => {
345- const href = safeHref ( url , rawBase ) ;
346- return href ? `<img src="${ href } " alt="${ alt } ">` : m ;
347- } ) ;
348- s = s . replace ( / \[ ( [ ^ \] ] + ) \] \( ( [ ^ ) \s ] + ) \) / g, ( m , text , url ) => {
349- const href = safeHref ( url , blobBase ) ;
350- return href ? `<a href="${ href } ">${ text } </a>` : m ;
351- } ) ;
352- s = s . replace ( / \* \* ( [ ^ * ] + ) \* \* / g, '<strong>$1</strong>' ) ;
353- s = s . replace ( / \* ( [ ^ * \s ] [ ^ * ] * ) \* / g, '<em>$1</em>' ) ;
354- s = s . replace ( / ( ^ | \s ) _ ( [ ^ _ ] + ) _ (? = \s | $ ) / g, '$1<em>$2</em>' ) ;
355- return s . replace ( / \x01 ( \d + ) \x01 / g, ( m , i ) => `<code>${ codes [ i ] ?? '' } </code>` ) ;
356- }
357-
358- function renderMarkdown ( src , ctx ) {
359- const lines = String ( src ) . replace ( / [ \x00 \x01 ] / g, '' ) . replace ( / \r \n ? / g, '\n' ) . split ( '\n' ) ;
360- const out = [ ] ;
361- let para = [ ] ;
362- let list = null ; // { tag, items }
363- let quote = [ ] ;
364- const flushPara = ( ) => { if ( para . length ) { out . push ( `<p>${ mdInline ( esc ( para . join ( ' ' ) ) , ctx ) } </p>` ) ; para = [ ] ; } } ;
365- const flushList = ( ) => { if ( list ) { out . push ( `<${ list . tag } >${ list . items . map ( ( i ) => `<li>${ i } </li>` ) . join ( '' ) } </${ list . tag } >` ) ; list = null ; } } ;
366- const flushQuote = ( ) => { if ( quote . length ) { out . push ( `<blockquote><p>${ mdInline ( esc ( quote . join ( ' ' ) ) , ctx ) } </p></blockquote>` ) ; quote = [ ] ; } } ;
367- const flushAll = ( ) => { flushPara ( ) ; flushList ( ) ; flushQuote ( ) ; } ;
368-
369- for ( let i = 0 ; i < lines . length ; i ++ ) {
370- const line = lines [ i ] ;
371- const fence = / ^ ` ` ` / . exec ( line ) ;
372- if ( fence ) {
373- flushAll ( ) ;
374- const code = [ ] ;
375- i += 1 ;
376- while ( i < lines . length && ! / ^ ` ` ` / . test ( lines [ i ] ) ) { code . push ( lines [ i ] ) ; i += 1 ; }
377- out . push ( `<pre><code>${ esc ( code . join ( '\n' ) ) } </code></pre>` ) ;
378- continue ;
379- }
380- const h = / ^ ( # { 1 , 6 } ) \s + ( .* ) $ / . exec ( line ) ;
381- if ( h ) { flushAll ( ) ; out . push ( `<h${ h [ 1 ] . length } >${ mdInline ( esc ( h [ 2 ] . trim ( ) ) , ctx ) } </h${ h [ 1 ] . length } >` ) ; continue ; }
382- const q = / ^ > \s ? ( .* ) $ / . exec ( line ) ;
383- if ( q ) { flushPara ( ) ; flushList ( ) ; quote . push ( q [ 1 ] ) ; continue ; }
384- const ul = / ^ \s * [ - * ] \s + ( .* ) $ / . exec ( line ) ;
385- if ( ul ) {
386- flushPara ( ) ; flushQuote ( ) ;
387- if ( ! list || list . tag !== 'ul' ) { flushList ( ) ; list = { tag : 'ul' , items : [ ] } ; }
388- list . items . push ( mdInline ( esc ( ul [ 1 ] ) , ctx ) ) ;
389- continue ;
390- }
391- const ol = / ^ \s * \d + \. \s + ( .* ) $ / . exec ( line ) ;
392- if ( ol ) {
393- flushPara ( ) ; flushQuote ( ) ;
394- if ( ! list || list . tag !== 'ol' ) { flushList ( ) ; list = { tag : 'ol' , items : [ ] } ; }
395- list . items . push ( mdInline ( esc ( ol [ 1 ] ) , ctx ) ) ;
396- continue ;
397- }
398- if ( / ^ \s * $ / . test ( line ) ) { flushAll ( ) ; continue ; }
399- flushList ( ) ; flushQuote ( ) ;
400- para . push ( line . trim ( ) ) ;
401- }
402- flushAll ( ) ;
403- return out . join ( '\n' ) ;
404- }
321+ // markdown renderer (escape-first grammar + GFM tables) moved to
322+ // ./lib/markdown.js — renderMarkdown imported above.
405323
406324// ------------------------------------------------------------- diff parse
407325// ONE structured parse feeds both the HTML renderer and the JSON API's
@@ -524,6 +442,10 @@ table.files td.age{color:#59636e;text-align:right;white-space:nowrap}
524442.markdown-body code{background:rgba(129,139,152,0.12);border-radius:6px;padding:.2em .4em;font-size:85%}
525443.markdown-body pre{background:#f6f8fa;border-radius:6px;padding:16px;overflow:auto;font-size:85%;line-height:1.45}
526444.markdown-body pre code{background:transparent;padding:0;font-size:100%}
445+ .markdown-body table{border-collapse:collapse;margin:0 0 16px;display:block;width:max-content;max-width:100%;overflow-x:auto}
446+ .markdown-body th,.markdown-body td{border:1px solid #d0d7de;padding:6px 13px}
447+ .markdown-body th{background:#f6f8fa;font-weight:600}
448+ .markdown-body tr:nth-child(2n) td{background:#f6f8fa}
527449.markdown-body blockquote{margin:0 0 16px;padding:0 1em;color:#59636e;border-left:.25em solid #d0d7de}
528450.markdown-body img{max-width:100%}
529451.clonebox{display:flex;gap:8px;align-items:center}
0 commit comments