@@ -31,27 +31,27 @@ function isBlank(line: string) {
31
31
}
32
32
33
33
/**
34
- * Turns a series of source lines into a standalone snippet file by:
35
- * - Converting require statements into top-level imports.
36
- * - Adjusting indentation to left-align all content
37
- * - Removing any blank lines at the starts
38
- * - Adding a suffix to snippet names
39
- *
40
- * @param lines the lines containing the snippet (including START/END comments)
41
- * @param sourceFile the source file where the original snippet lives
42
- * @param snippetSuffix the suffix (such as _modular)
34
+ * Replace all const { foo } = require('bar') with import { foo } from 'bar';
43
35
*/
44
- function processSnippet (
45
- lines : string [ ] ,
46
- sourceFile : string ,
47
- snippetSuffix : string
48
- ) : string {
49
- const outputLines : string [ ] = [ ] ;
50
-
36
+ function replaceRequireWithImport ( lines : string [ ] ) {
37
+ const outputLines = [ ] ;
51
38
for ( const line of lines ) {
52
39
if ( line . match ( RE_REQUIRE ) ) {
53
40
outputLines . push ( line . replace ( RE_REQUIRE , `import {$1} from $2` ) ) ;
54
- } else if ( line . match ( RE_START_SNIPPET ) ) {
41
+ } else {
42
+ outputLines . push ( line ) ;
43
+ }
44
+ }
45
+ return outputLines ;
46
+ }
47
+
48
+ /**
49
+ * Change all [START foo] and [END foo] to be [START foosuffix] and [END foosuffix]
50
+ */
51
+ function addSuffixToSnippetNames ( lines : string [ ] , snippetSuffix : string ) {
52
+ const outputLines = [ ] ;
53
+ for ( const line of lines ) {
54
+ if ( line . match ( RE_START_SNIPPET ) ) {
55
55
outputLines . push ( line . replace ( RE_START_SNIPPET , `[START $1${ snippetSuffix } ]` ) ) ;
56
56
} else if ( line . match ( RE_END_SNIPPET ) ) {
57
57
outputLines . push (
@@ -61,37 +61,74 @@ function processSnippet(
61
61
outputLines . push ( line ) ;
62
62
}
63
63
}
64
+ return outputLines ;
65
+ }
64
66
65
- // Adjust indentation of the otherLines so that they're left aligned
66
- const nonBlankLines = outputLines . filter ( ( l ) => ! isBlank ( l ) ) ;
67
+ /**
68
+ * Remove all left-padding so that the least indented line is left-aligned.
69
+ */
70
+ function adjustIndentation ( lines : string [ ] ) {
71
+ const nonBlankLines = lines . filter ( ( l ) => ! isBlank ( l ) ) ;
67
72
const indentSizes = nonBlankLines . map ( ( l ) => l . length - l . trimLeft ( ) . length ) ;
68
73
const minIndent = Math . min ( ...indentSizes ) ;
69
74
70
- const adjustedOutputLines : string [ ] = [ ] ;
71
- for ( const line of outputLines ) {
75
+ const outputLines = [ ] ;
76
+ for ( const line of lines ) {
72
77
if ( isBlank ( line ) ) {
73
- adjustedOutputLines . push ( "" ) ;
78
+ outputLines . push ( "" ) ;
74
79
} else {
75
- adjustedOutputLines . push ( line . substr ( minIndent ) ) ;
80
+ outputLines . push ( line . substr ( minIndent ) ) ;
76
81
}
77
82
}
83
+ return outputLines ;
84
+ }
78
85
79
- // Special case: if the first line after the comments is blank we want to remove it
80
- const firstNonComment = adjustedOutputLines . findIndex (
86
+ /**
87
+ * If the first line after leading comments is blank, remove it.
88
+ */
89
+ function removeFirstLineAfterComments ( lines : string [ ] ) {
90
+ const outputLines = [ ...lines ] ;
91
+
92
+ const firstNonComment = outputLines . findIndex (
81
93
( l ) => ! l . startsWith ( "//" )
82
94
) ;
83
95
if ( isBlank ( outputLines [ firstNonComment ] ) ) {
84
- adjustedOutputLines . splice ( firstNonComment , 1 ) ;
96
+ outputLines . splice ( firstNonComment , 1 ) ;
85
97
}
86
98
99
+ return outputLines ;
100
+ }
101
+
102
+ /**
103
+ * Turns a series of source lines into a standalone snippet file by running
104
+ * a series of transformations.
105
+ *
106
+ * @param lines the lines containing the snippet (including START/END comments)
107
+ * @param sourceFile the source file where the original snippet lives (used in preamble)
108
+ * @param snippetSuffix the suffix (such as _modular)
109
+ */
110
+ function processSnippet (
111
+ lines : string [ ] ,
112
+ sourceFile : string ,
113
+ snippetSuffix : string
114
+ ) : string {
115
+ let outputLines = [ ...lines ] ;
116
+
117
+ // Perform transformations individually, in order
118
+ outputLines = replaceRequireWithImport ( outputLines ) ;
119
+ outputLines = addSuffixToSnippetNames ( outputLines , snippetSuffix ) ;
120
+ outputLines = adjustIndentation ( outputLines ) ;
121
+ outputLines = removeFirstLineAfterComments ( outputLines ) ;
122
+
123
+ // Add a preamble to every snippet
87
124
const preambleLines = [
88
125
`// This snippet file was generated by processing the source file:` ,
89
126
`// ${ sourceFile } ` ,
90
127
`//` ,
91
128
`// To make edits to the snippets in this file, please edit the source` ,
92
129
`` ,
93
130
] ;
94
- const content = [ ...preambleLines , ...adjustedOutputLines ] . join ( "\n" ) ;
131
+ const content = [ ...preambleLines , ...outputLines ] . join ( "\n" ) ;
95
132
return content ;
96
133
}
97
134
@@ -121,40 +158,55 @@ function collectSnippets(filePath: string): SnippetsConfig {
121
158
map : { } ,
122
159
} ;
123
160
161
+ // If a file does not have '// [SNIPPETS_SEPARATION enabled]' in it then
162
+ // we don't process it for this script.
124
163
config . enabled = lines . some ( ( l ) => ! ! l . match ( RE_SNIPPETS_SEPARATION ) ) ;
125
164
if ( ! config . enabled ) {
126
165
return config ;
127
166
}
128
167
168
+ // If the file contains '// [SNIPPETS_SUFFIX _banana]' we use _banana (or whatever)
169
+ // as the suffix. Otherwise we default to _modular.
129
170
const suffixLine = lines . find ( ( l ) => ! ! l . match ( RE_SNIPPETS_SUFFIX ) ) ;
130
171
if ( suffixLine ) {
131
172
const m = suffixLine . match ( RE_SNIPPETS_SUFFIX ) ;
132
173
config . suffix = m [ 1 ] ;
133
174
}
134
175
176
+ // A temporary array holding the names of snippets we're currently within.
177
+ // This allows for handling nested snippets.
135
178
let inSnippetNames = [ ] ;
136
179
137
180
for ( const line of lines ) {
138
181
const startMatch = line . match ( RE_START_SNIPPET ) ;
139
182
const endMatch = line . match ( RE_END_SNIPPET ) ;
140
183
141
184
if ( startMatch ) {
185
+ // When we find a new [START foo] tag we are now inside snippet 'foo'.
186
+ // Until we find an [END foo] tag. All lines we see between now and then
187
+ // are part of the snippet content.
142
188
const snippetName = startMatch [ 1 ] ;
143
- config . map [ snippetName ] = [ ] ;
144
- config . map [ snippetName ] . push ( line ) ;
145
-
189
+ config . map [ snippetName ] = [ line ] ;
146
190
inSnippetNames . push ( snippetName ) ;
147
191
} else if ( endMatch ) {
192
+ // When we find a new [END foo] tag we are now exiting snippet 'foo'.
148
193
const snippetName = endMatch [ 1 ] ;
149
- config . map [ snippetName ] . push ( line ) ;
150
194
195
+ // If we were not aware that we were inside this snippet (no previous START)
196
+ // then we hard throw.
151
197
if ( ! inSnippetNames . includes ( snippetName ) ) {
152
198
throw new Error (
153
199
`Unrecognized END tag ${ snippetName } in ${ filePath } .`
154
200
) ;
155
201
}
202
+
203
+ // Collect this line as the final line of the snippet and then
204
+ // remove this snippet name from the list we're tracking.
205
+ config . map [ snippetName ] . push ( line ) ;
156
206
inSnippetNames . splice ( inSnippetNames . indexOf ( snippetName ) , 1 ) ;
157
207
} else if ( inSnippetNames . length > 0 ) {
208
+ // Any line that is not START or END is appended to the list of
209
+ // lines for all the snippets we're currently tracking.
158
210
for ( const snippetName of inSnippetNames ) {
159
211
config . map [ snippetName ] . push ( line ) ;
160
212
}
@@ -189,11 +241,14 @@ async function main() {
189
241
190
242
for ( const snippetName in config . map ) {
191
243
const newFilePath = path . join ( snippetDir , `${ snippetName } .js` ) ;
244
+
245
+ const snippetLines = config . map [ snippetName ] ;
192
246
const content = processSnippet (
193
- config . map [ snippetName ] ,
247
+ snippetLines ,
194
248
filePath ,
195
249
config . suffix
196
250
) ;
251
+
197
252
fs . writeFileSync ( newFilePath , content ) ;
198
253
}
199
254
}
0 commit comments