11const rasterisks = / ^ \s * \* /
2+ const rblockquote = / ^ \s * & g t ; /
23const rheaders = / ^ * ( \= + ) * ( [ ^ \n \r ] + ?) [ = \s ] * $ /
4+
5+ const space = '(^|\\s)'
6+ const quoted = ' "([^"]+)"'
7+ const bracketed = '(?: ([^\\]]+))?'
8+
9+ const urlpart = '[^\\s\\]]+'
10+ const url = `(https?://${ urlpart } )`
11+ const relativeLink = `(/${ urlpart } )`
12+ const hashLink = `(#${ urlpart } )`
13+ const camelCaseLink = `([A-Z][a-z]+[A-Z]${ urlpart } )`
14+ const tracLink = `trac:(${ urlpart } )`
15+ const wikiLink = `wiki:(${ urlpart } )`
16+ const wikipediaLink = `wikipedia:(${ urlpart } )`
17+
318let listStarted = false
19+ let blockquoteStarted = false
20+
21+ const excludeMacros = [ 'br' , 'tracguidetoc' , 'pageoutline' ]
422
523function escapeHTML ( string ) {
624 return string . replace ( / < / g, '<' ) . replace ( / > / g, '>' )
@@ -9,127 +27,204 @@ function escapeHTML(string) {
927module . exports = function tracToHTML ( text ) {
1028 const codes = [ ]
1129 const pres = [ ]
12- return (
13- escapeHTML ( text )
14- // Newlines have extra escapes in the strings
15- . replace ( / \\ \n / g, '\n' )
16- // Replace `` with <code> tags
17- . replace ( / ` ( [ ^ \r \n ` ] + ?) ` / g, ( _match , code ) => {
18- codes . push ( code ) // Save the code for later
19- return `<code></code>`
20- } )
21- // Replace {{{ }}} with <pre> tags
22- . replace ( / { { { ( [ ^ ] + ?) } } } / g, ( _match , code ) => {
23- // Save the code for later
24- pres . push (
25- // Remove language hints
26- code . replace ( / ^ # ! \w + \r ? \n / , '' )
27- )
28- return `<pre class="wiki"></pre>`
29- } )
30- // Linkify http links outside brackets
31- . replace ( / ( ^ | \s ) ( h t t p s ? : \/ \/ [ ^ \s ] + ) / g, function ( _match , space , url ) {
32- return `${
33- space || ''
34- } <a href="${ url } " class="ext-link"><span class="icon"></span>${ url } </a>`
35- } )
36- // Linkify http links in brackets
37- . replace (
38- / ( ^ | \s ) (?: (?: \[ h t t p s ? : \/ \/ ( [ ^ ] + ) " ( [ ^ " ] + ) " \] ) | (?: \[ h t t p s ? : \/ \/ ( [ ^ \s \] ] + ) (?: ( [ ^ \] ] + ) ) ? \] ) ) / g,
39- function ( _match , space , quotedurl , quotedtext , url , text ) {
40- return `${ space || '' } <a href="${
41- quotedurl || url
42- } " class="ext-link"><span class="icon"></span>${
43- quotedtext || text || url
44- } </a>`
45- }
46- )
47- // Linkify hash links in brackets
48- . replace (
49- / ( ^ | \s ) (?: (?: \[ ( # [ ^ ] + ) " ( [ ^ " ] + ) " \] ) | (?: \[ ( # [ ^ \s \] ] + ) (?: ( [ ^ \] ] + ) ) ? \] ) ) / g,
50- function ( _match , space , quotedurl , quotedtext , url , text ) {
51- return `${ space || '' } <a href="${
52- quotedurl || url
53- } " class="ext-link"><span class="icon"></span>${
54- quotedtext || text || url
55- } </a>`
56- }
57- )
58- // Linkify CamelCase links in brackets
59- . replace (
60- / ( ^ | \s ) (?: (?: \[ ( [ A - Z ] [ a - z ] + [ A - Z ] [ ^ ] + ) " ( [ ^ " ] + ) " \] ) | (?: \[ ( [ A - Z ] [ a - z ] + [ A - Z ] [ ^ \s \] ] + ) (?: ( [ ^ \] ] + ) ) ? \] ) ) / g,
61- function ( _match , space , quotedpage , quotedtext , page , text ) {
62- return `${ space || '' } <a href="/wiki/${ quotedpage || page } ">${
63- quotedtext || text || page
64- } </a>`
65- }
30+ let html = escapeHTML ( text )
31+ // Newlines have extra escapes in the strings
32+ . replace ( / \\ \n / g, '\n' )
33+ // Replace `` with <code> tags
34+ . replace ( / ` ( [ ^ \r \n ` ] + ?) ` / g, ( _match , code ) => {
35+ codes . push ( code ) // Save the code for later
36+ return `<code></code>`
37+ } )
38+ // Replace {{{ }}} with <pre> tags
39+ . replace ( / { { { ( [ ^ ] + ?) } } } / g, ( _match , code ) => {
40+ // Save the code for later
41+ pres . push (
42+ // Remove language hints
43+ code . replace ( / ^ # ! \w + \r ? \n / , '' )
6644 )
67- // Linkify trac links
68- . replace (
69- / ( ^ | \s ) (?: (?: \[ t r a c : ( [ ^ ] + ) " ( [ ^ " ] + ) " \] ) | (?: \[ t r a c : ( [ ^ \s \] ] + ) (?: ( [ ^ \] ] + ) ) ? \] ) ) / g,
70- function ( _match , space , quotepage , quotedtext , page , text ) {
71- return `${ space || '' } <a href="https://trac.edgewall.org/intertrac/${
72- quotepage || page
73- } " class="ext-link"><span class="icon"></span>${
74- quotedtext || text || page
75- } </a>`
45+ return `<pre class="wiki"></pre>`
46+ } )
47+ // Linkify http links outside brackets
48+ . replace ( new RegExp ( `${ space } ${ url } ` , 'g' ) , function ( _match , space , url ) {
49+ return `${
50+ space || ''
51+ } <a href="${ url } " class="ext-link"><span class="icon"></span>${ url } </a>`
52+ } )
53+ // Linkify http links in brackets
54+ . replace (
55+ new RegExp (
56+ `${ space } (?:(?:\\[${ url } ${ quoted } \\])|(?:\\[${ url } ${ bracketed } \\]))` ,
57+ 'g'
58+ ) ,
59+ function ( _match , space , quotedurl , quotedtext , url , text ) {
60+ return `${ space || '' } <a href="${
61+ quotedurl || url
62+ } " class="ext-link"><span class="icon"></span>${
63+ quotedtext || text || url
64+ } </a>`
65+ }
66+ )
67+ // Linkify relative links in brackets
68+ . replace (
69+ new RegExp (
70+ `${ space } (?:(?:\\[${ relativeLink } ${ quoted } \\])|(?:\\[${ relativeLink } ${ bracketed } \\]))` ,
71+ 'g'
72+ ) ,
73+ function ( _match , space , quotedurl , quotedtext , url , text ) {
74+ return `${ space || '' } <a href="${
75+ quotedurl || url
76+ } " class="ext-link"><span class="icon"></span>${
77+ quotedtext || text || url
78+ } </a>`
79+ }
80+ )
81+ // Linkify hash links in brackets
82+ . replace (
83+ new RegExp (
84+ `${ space } (?:(?:\\[${ hashLink } ${ quoted } \\])|(?:\\[${ hashLink } ${ bracketed } \\]))` ,
85+ 'g'
86+ ) ,
87+ function ( _match , space , quotedurl , quotedtext , url , text ) {
88+ return `${ space || '' } <a href="${
89+ quotedurl || url
90+ } " class="ext-link"><span class="icon"></span>${
91+ quotedtext || text || url
92+ } </a>`
93+ }
94+ )
95+ // Linkify CamelCase links in brackets
96+ . replace (
97+ new RegExp (
98+ `${ space } (?:(?:\\[${ camelCaseLink } ${ quoted } \\])|(?:\\[${ camelCaseLink } ${ bracketed } \\]))` ,
99+ 'g'
100+ ) ,
101+ function ( _match , space , quotedpage , quotedtext , page , text ) {
102+ return `${ space || '' } <a href="/wiki/${ quotedpage || page } ">${
103+ quotedtext || text || page
104+ } </a>`
105+ }
106+ )
107+ // Linkify trac links
108+ . replace (
109+ new RegExp (
110+ `${ space } (?:(?:\\[${ tracLink } ${ quoted } \\])|(?:\\[${ tracLink } ${ bracketed } \\]))` ,
111+ 'ig'
112+ ) ,
113+ function ( _match , space , quotepage , quotedtext , page , text ) {
114+ return `${ space || '' } <a href="https://trac.edgewall.org/intertrac/${
115+ quotepage || page
116+ } " class="ext-link"><span class="icon"></span>${
117+ quotedtext || text || page
118+ } </a>`
119+ }
120+ )
121+ // Linkify wiki links
122+ . replace (
123+ new RegExp (
124+ `${ space } (?:(?:\\[${ wikiLink } ${ quoted } \\])|(?:\\[${ wikiLink } ${ bracketed } \\]))` ,
125+ 'ig'
126+ ) ,
127+ function ( _match , space , quotepage , quotedtext , page , text ) {
128+ return `${ space || '' } <a href="/wiki/${
129+ quotepage || page
130+ } " class="ext-link"><span class="icon"></span>${
131+ quotedtext || text || page
132+ } </a>`
133+ }
134+ )
135+ // Linkify wikipedia links
136+ . replace (
137+ new RegExp (
138+ `${ space } (?:(?:\\[${ wikipediaLink } ${ quoted } \\])|(?:\\[${ wikipediaLink } ${ bracketed } \\]))` ,
139+ 'ig'
140+ ) ,
141+ function ( _match , space , quotepage , quotedtext , page , text ) {
142+ return `${ space || '' } <a href="https://wikipedia.org/wiki/${
143+ quotepage || page
144+ } " class="ext-link"><span class="icon"></span>${
145+ quotedtext || text || page
146+ } </a>`
147+ }
148+ )
149+ // Linkify ticket references (avoid trac ticket links)
150+ . replace ( / # ( \d + ) (? ! < = > ) / g, `<a href="/ticket/$1">$&</a>` )
151+ // Linkify CamelCase to wiki
152+ . replace (
153+ new RegExp ( `${ space } (!)?${ camelCaseLink } ` , 'g' ) ,
154+ function ( _match , space , excl , page ) {
155+ if ( excl ) {
156+ return `${ space || '' } ${ page } `
76157 }
77- )
78- // Linkify ticket references (avoid trac ticket links)
79- . replace ( / # ( \d + ) (? ! < = > ) / g, `<a href="/ticket/$1">$&</a>` )
80- // Linkify CamelCase to wiki
81- . replace (
82- / ( ^ | \s ) ( ! ) ? ( [ A - Z ] [ a - z ] + [ A - Z ] [ \w : ] + (?: # \w + ) ? ) (? ! \w ) / g,
83- function ( _match , space , excl , page ) {
84- if ( excl ) {
85- return `${ space || '' } ${ page } `
86- }
87- return `${ space || '' } <a href="/wiki/${ page } ">${ page } </a>`
88- }
89- )
90- // Convert ---- to <hr>
91- . replace ( / ^ - - + $ / gm, '<hr />' )
92- // Replace three single quotes with <strong>
93- . replace ( / ' ' ' ( [ ^ ' ] + ) ' ' ' / g, '<strong>$1</strong>' )
94- // Replace double newlines with paragraphs
95- . split ( / (?: \r ? \n ) / g)
96- . map ( ( line ) => {
97- let ret = ''
98- if ( listStarted && ! rasterisks . test ( line ) ) {
99- listStarted = false
100- ret += '</ul>'
101- }
102- if ( ! line . trim ( ) ) {
103- return ret
104- }
105- if ( line . startsWith ( '<pre' ) ) {
106- return ret + line
107- }
108- // Blockquotes
109- if ( line . startsWith ( '> ' ) ) {
110- return ret + `<blockquote>${ line . slice ( 2 ) } </blockquote>`
111- }
112- // Headers
113- if ( rheaders . test ( line ) ) {
114- return (
115- ret +
116- line . replace ( rheaders , ( _all , equals , content ) => {
117- const level = equals . length
118- return `<h${ level } >${ content } </h${ level } >`
119- } )
120- )
158+ return `${ space || '' } <a href="/wiki/${ page } ">${ page } </a>`
159+ }
160+ )
161+ // Convert ---- to <hr>
162+ . replace ( / ^ - - + $ / gm, '<hr />' )
163+ // Replace three single quotes with <strong>
164+ . replace ( / ' ' ' ( [ ^ ' ] + ) ' ' ' / g, '<strong>$1</strong>' )
165+ // Remove certain trac macros
166+ . replace ( / \[ \[ ( [ ^ \] ] + ) \] \] / g, function ( match , name ) {
167+ for ( const macro in excludeMacros ) {
168+ if ( name . toLowerCase ( ) . startsWith ( excludeMacros [ macro ] ) ) return ''
169+ }
170+ return match
171+ } )
172+ // Replace double newlines with paragraphs
173+ . split ( / (?: \r ? \n ) / g)
174+ . map ( ( line ) => {
175+ let ret = ''
176+ if ( listStarted && ! rasterisks . test ( line ) ) {
177+ listStarted = false
178+ ret += '</ul>'
179+ } else if ( blockquoteStarted && ! rblockquote . test ( line ) ) {
180+ blockquoteStarted = false
181+ ret += '</blockquote>'
182+ }
183+ if ( ! line . trim ( ) ) {
184+ return ret
185+ }
186+ if ( line . startsWith ( '<pre' ) ) {
187+ return ret + line
188+ }
189+ // Blockquotes
190+ if ( rblockquote . test ( line ) ) {
191+ if ( ! blockquoteStarted ) {
192+ blockquoteStarted = true
193+ ret += '<blockquote>'
121194 }
122- if ( rasterisks . test ( line ) ) {
123- line = line . replace (
124- / ( ^ | \s + ) \* ( [ ^ \n ] + ) / g,
125- `$1${ listStarted ? '' : '<ul>' } <li>$2</li>`
126- )
127- listStarted = true
128- return ret + line
129- }
130- return ret + `<p>${ line } </p>`
131- } )
132- . join ( '' )
195+ return ret + line . replace ( rblockquote , ' ' )
196+ }
197+ // Headers
198+ if ( rheaders . test ( line ) ) {
199+ return (
200+ ret +
201+ line . replace ( rheaders , ( _all , equals , content ) => {
202+ const level = equals . length
203+ return `<h${ level } >${ content } </h${ level } >`
204+ } )
205+ )
206+ }
207+ if ( rasterisks . test ( line ) ) {
208+ line = line . replace (
209+ / ( ^ | \s + ) \* ( [ ^ \n ] + ) / g,
210+ `$1${ listStarted ? '' : '<ul>' } <li>$2</li>`
211+ )
212+ listStarted = true
213+ return ret + line
214+ }
215+ return ret + `<p>${ line } </p>`
216+ } )
217+ . join ( '' )
218+
219+ if ( listStarted ) {
220+ html += '</ul>'
221+ }
222+ if ( blockquoteStarted ) {
223+ html += '</blockquote>'
224+ }
225+
226+ return (
227+ html
133228 // Reinsert code
134229 . replace ( / < c o d e > < \/ c o d e > / g, ( ) => {
135230 const code = codes . shift ( )
0 commit comments