1
1
const rasterisks = / ^ \s * \* /
2
+ const rblockquote = / ^ \s * & g t ; /
2
3
const 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
+
3
18
let listStarted = false
19
+ let blockquoteStarted = false
20
+
21
+ const excludeMacros = [ 'br' , 'tracguidetoc' , 'pageoutline' ]
4
22
5
23
function escapeHTML ( string ) {
6
24
return string . replace ( / < / g, '<' ) . replace ( / > / g, '>' )
@@ -9,127 +27,204 @@ function escapeHTML(string) {
9
27
module . exports = function tracToHTML ( text ) {
10
28
const codes = [ ]
11
29
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 / , '' )
66
44
)
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 } `
76
157
}
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>'
121
194
}
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
133
228
// Reinsert code
134
229
. replace ( / < c o d e > < \/ c o d e > / g, ( ) => {
135
230
const code = codes . shift ( )
0 commit comments