comment-block-transformer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -0
- package/_usage.js +60 -0
- package/package.json +26 -0
- package/src/index.js +396 -0
- package/test/index.test.js +319 -0
- package/test/utils/log.js +20 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# comment-block-transformer
|
|
2
|
+
|
|
3
|
+
Transform markdown blocks based on configured transforms with middleware support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install comment-block-transformer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
The block transformer allows you to process markdown blocks with custom transforms and middleware functions.
|
|
14
|
+
|
|
15
|
+
### Basic Usage
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const { blockTransformer } = require('comment-block-transformer')
|
|
19
|
+
|
|
20
|
+
const text = `
|
|
21
|
+
<!-- block example -->
|
|
22
|
+
Some content to transform
|
|
23
|
+
<!-- /block -->
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
const config = {
|
|
27
|
+
transforms: {
|
|
28
|
+
example: ({ content }) => {
|
|
29
|
+
return content.toUpperCase()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await blockTransformer(text, config)
|
|
35
|
+
console.log(result.updatedContents)
|
|
36
|
+
// Output: Content will be transformed to uppercase
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Transform with Options
|
|
40
|
+
|
|
41
|
+
You can pass options to your transforms:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
const text = `
|
|
45
|
+
<!-- block prefix {"prefix": "NOTE: "} -->
|
|
46
|
+
This will get a prefix
|
|
47
|
+
<!-- /block -->
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
const config = {
|
|
51
|
+
transforms: {
|
|
52
|
+
prefix: ({ content, options }) => {
|
|
53
|
+
return `${options.prefix || 'PREFIX: '}${content}`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Multiple Transforms
|
|
60
|
+
|
|
61
|
+
You can use multiple transforms in the same document:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const text = `
|
|
65
|
+
<!-- block upperCase -->
|
|
66
|
+
hello world
|
|
67
|
+
<!-- /block -->
|
|
68
|
+
|
|
69
|
+
<!-- block reverse -->
|
|
70
|
+
abc def
|
|
71
|
+
<!-- /block -->
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
const config = {
|
|
75
|
+
transforms: {
|
|
76
|
+
upperCase: ({ content }) => content.toUpperCase(),
|
|
77
|
+
reverse: ({ content }) => content.split('').reverse().join('')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await blockTransformer(text, config)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Middleware Support
|
|
85
|
+
|
|
86
|
+
The block transformer supports both `beforeMiddleware` and `afterMiddleware` to process content before and after transforms are applied.
|
|
87
|
+
|
|
88
|
+
#### Before Middleware
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
const beforeMiddleware = [
|
|
92
|
+
{
|
|
93
|
+
name: 'addPrefix',
|
|
94
|
+
transform: (blockData) => {
|
|
95
|
+
return `PREFIX: ${blockData.content.value}`
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'upperCase',
|
|
100
|
+
transform: (blockData) => {
|
|
101
|
+
return blockData.content.value.toUpperCase()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const config = {
|
|
107
|
+
transforms: {
|
|
108
|
+
example: (api) => api.content
|
|
109
|
+
},
|
|
110
|
+
beforeMiddleware
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### After Middleware
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
const afterMiddleware = [
|
|
118
|
+
{
|
|
119
|
+
name: 'addSuffix',
|
|
120
|
+
transform: (blockData) => {
|
|
121
|
+
return `${blockData.content.value} - PROCESSED`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
const config = {
|
|
127
|
+
transforms: {
|
|
128
|
+
example: (api) => api.content.trim()
|
|
129
|
+
},
|
|
130
|
+
afterMiddleware
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Combined Middleware
|
|
135
|
+
|
|
136
|
+
You can use both before and after middleware together:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const config = {
|
|
140
|
+
transforms: {
|
|
141
|
+
example: (api) => `_${api.content.toUpperCase()}_`
|
|
142
|
+
},
|
|
143
|
+
beforeMiddleware: [
|
|
144
|
+
{
|
|
145
|
+
name: 'addBefore',
|
|
146
|
+
transform: (blockData) => `BEFORE_${blockData.content.value}`
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
afterMiddleware: [
|
|
150
|
+
{
|
|
151
|
+
name: 'addAfter',
|
|
152
|
+
transform: (blockData) => `${blockData.content.value}_AFTER`
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Custom Delimiters
|
|
159
|
+
|
|
160
|
+
You can customize the block delimiters:
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const text = `
|
|
164
|
+
<!-- CUSTOM:START test -->
|
|
165
|
+
Some content
|
|
166
|
+
<!-- CUSTOM:END -->
|
|
167
|
+
`
|
|
168
|
+
|
|
169
|
+
const config = {
|
|
170
|
+
open: 'CUSTOM:START',
|
|
171
|
+
close: 'CUSTOM:END',
|
|
172
|
+
transforms: {
|
|
173
|
+
test: (api) => api.content.toUpperCase()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Custom Regex Patterns
|
|
179
|
+
|
|
180
|
+
You can provide custom regex patterns for parsing:
|
|
181
|
+
|
|
182
|
+
```javascript
|
|
183
|
+
const config = {
|
|
184
|
+
customPatterns: {
|
|
185
|
+
open: /<!--\s*CUSTOM:START\s+(\w+)(?:\s+(\{.*?\}))?\s*-->/g,
|
|
186
|
+
close: /<!--\s*CUSTOM:END\s*-->/g
|
|
187
|
+
},
|
|
188
|
+
transforms: {
|
|
189
|
+
test: (api) => api.content.toUpperCase()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## API Reference
|
|
195
|
+
|
|
196
|
+
### blockTransformer(inputText, config)
|
|
197
|
+
|
|
198
|
+
Transform markdown blocks based on configured transforms.
|
|
199
|
+
|
|
200
|
+
#### Parameters
|
|
201
|
+
|
|
202
|
+
- `inputText` (string): The text content to process
|
|
203
|
+
- `config` (ProcessContentConfig): Configuration options
|
|
204
|
+
|
|
205
|
+
#### Returns
|
|
206
|
+
|
|
207
|
+
Promise<BlockTransformerResult> - Result object containing transformed content and metadata
|
|
208
|
+
|
|
209
|
+
### ProcessContentConfig
|
|
210
|
+
|
|
211
|
+
Configuration object for processing contents.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
interface ProcessContentConfig {
|
|
215
|
+
open?: string // Opening delimiter (default: 'block')
|
|
216
|
+
close?: string // Closing delimiter (default: '/block')
|
|
217
|
+
syntax?: string // Syntax type (default: 'md')
|
|
218
|
+
transforms?: TransformerPlugins // Transform functions
|
|
219
|
+
beforeMiddleware?: Middleware[] // Middleware functions applied before transforms
|
|
220
|
+
afterMiddleware?: Middleware[] // Middleware functions applied after transforms
|
|
221
|
+
removeComments?: boolean // Remove comments from output (default: false)
|
|
222
|
+
srcPath?: string // Source file path
|
|
223
|
+
outputPath?: string // Output file path
|
|
224
|
+
customPatterns?: CustomPatterns // Custom regex patterns for open and close tags
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### TransformFunction
|
|
229
|
+
|
|
230
|
+
Transform function signature:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
type TransformFunction = (api: TransformApi) => Promise<string> | string
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### TransformApi
|
|
237
|
+
|
|
238
|
+
The API object passed to transform functions:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
interface TransformApi {
|
|
242
|
+
transform: string // Name of the transform
|
|
243
|
+
content: string // Content to transform
|
|
244
|
+
options: object // Transform options
|
|
245
|
+
srcPath?: string // Source file path
|
|
246
|
+
outputPath?: string // Output file path
|
|
247
|
+
settings: object // Additional settings including regex patterns
|
|
248
|
+
currentContent: string // Current file contents
|
|
249
|
+
originalContent: string // Original file contents
|
|
250
|
+
getCurrentContent(): string // Function to get current file contents
|
|
251
|
+
getOriginalContent(): string // Function to get original file contents
|
|
252
|
+
getOriginalBlock(): object // Function to get the original block data
|
|
253
|
+
getBlockDetails(content?: string): object // Function to get detailed block information
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Middleware
|
|
258
|
+
|
|
259
|
+
Middleware function interface:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
interface Middleware {
|
|
263
|
+
name: string // Name of the middleware
|
|
264
|
+
transform: (blockData: BlockData, updatedText: string) => Promise<string> | string // Transform function
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### BlockTransformerResult
|
|
269
|
+
|
|
270
|
+
Result object returned by blockTransformer:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
interface BlockTransformerResult {
|
|
274
|
+
isChanged: boolean // Whether the content was changed by transforms
|
|
275
|
+
isNewPath: boolean // Whether srcPath differs from outputPath
|
|
276
|
+
stripComments: boolean // Whether to strip comments from output
|
|
277
|
+
srcPath?: string // Source file path
|
|
278
|
+
outputPath?: string // Output file path
|
|
279
|
+
transforms: BlockData[] // Array of transforms that were applied
|
|
280
|
+
missingTransforms: any[] // Array of transforms that were not found
|
|
281
|
+
originalContents: string // Original input text
|
|
282
|
+
updatedContents: string // Transformed output text
|
|
283
|
+
patterns?: object // Regex patterns used for parsing
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Development
|
|
288
|
+
|
|
289
|
+
### Scripts
|
|
290
|
+
|
|
291
|
+
- `npm test` - Run tests using uvu
|
|
292
|
+
- `npm run build` - Generate TypeScript declarations
|
|
293
|
+
- `npm run types` - Generate TypeScript declarations only
|
|
294
|
+
- `npm run clean` - Clean generated files
|
|
295
|
+
- `npm run publish` - Publish to npm
|
|
296
|
+
- `npm run release:patch` - Release patch version
|
|
297
|
+
- `npm run release:minor` - Release minor version
|
|
298
|
+
- `npm run release:major` - Release major version
|
|
299
|
+
|
|
300
|
+
### Dependencies
|
|
301
|
+
|
|
302
|
+
- `comment-block-parser` - Core parsing functionality
|
|
303
|
+
- `typescript` - TypeScript support (dev)
|
|
304
|
+
- `uvu` - Testing framework (dev)
|
|
305
|
+
|
|
306
|
+
## Testing
|
|
307
|
+
|
|
308
|
+
The package uses [uvu](https://github.com/lukeed/uvu) for testing:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npm test
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## TypeScript Support
|
|
315
|
+
|
|
316
|
+
This package includes TypeScript declarations. The types are automatically generated from JSDoc comments.
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
npm run build
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
MIT
|
package/_usage.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { blockTransformer } = require('./src')
|
|
2
|
+
const deepLog = require('./test/utils/log')
|
|
3
|
+
|
|
4
|
+
// Example transforms
|
|
5
|
+
const transforms = {
|
|
6
|
+
// Transform that uppercases content
|
|
7
|
+
uppercase: async (api) => {
|
|
8
|
+
console.log('api', api)
|
|
9
|
+
return api.content.toUpperCase()
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
// Transform that adds a prefix
|
|
13
|
+
prefix: async (api) => {
|
|
14
|
+
const { options } = api
|
|
15
|
+
return `${options.prefix || 'PREFIX: '}${api.content}`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Example markdown content with transform blocks
|
|
20
|
+
const content = `
|
|
21
|
+
# My Document
|
|
22
|
+
|
|
23
|
+
<!-- DOCS:START uppercase -->
|
|
24
|
+
This will be transformed to uppercase
|
|
25
|
+
<!-- DOCS:END -->
|
|
26
|
+
|
|
27
|
+
<!-- DOCS:START prefix {"prefix": "NOTE: "} -->
|
|
28
|
+
This will get a prefix
|
|
29
|
+
<!-- DOCS:END -->
|
|
30
|
+
|
|
31
|
+
Regular content that won't be transformed
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
// Process the content
|
|
35
|
+
async function runExample() {
|
|
36
|
+
try {
|
|
37
|
+
const result = await blockTransformer(content, {
|
|
38
|
+
srcPath: 'example.md',
|
|
39
|
+
outputPath: 'example.output.md',
|
|
40
|
+
transforms,
|
|
41
|
+
debug: true
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
console.log('result.updatedContents', result.updatedContents)
|
|
45
|
+
|
|
46
|
+
// deepLog(result)
|
|
47
|
+
|
|
48
|
+
console.log('Original content:', content)
|
|
49
|
+
console.log('\nTransformed content:', result.updatedContents)
|
|
50
|
+
console.log('\nTransform details:', {
|
|
51
|
+
isChanged: result.isChanged,
|
|
52
|
+
transformsApplied: result.transforms.length,
|
|
53
|
+
missingTransforms: result.missingTransforms.length
|
|
54
|
+
})
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error:', error.message)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
runExample()
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "comment-block-transformer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Transform markdown blocks based on configured transforms",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "types/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"comment-block-parser": "1.0.7"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"typescript": "^5.0.0",
|
|
12
|
+
"uvu": "^0.5.6"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "uvu test",
|
|
19
|
+
"build": "pnpm run types",
|
|
20
|
+
"types": "tsc --emitDeclarationOnly --outDir types",
|
|
21
|
+
"clean": "rimraf types",
|
|
22
|
+
"release:patch": "pnpm run build && pnpm version patch && pnpm publish",
|
|
23
|
+
"release:minor": "pnpm run build && pnpm version minor && pnpm publish",
|
|
24
|
+
"release:major": "pnpm run build && pnpm version major && pnpm publish"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
const { parseBlocks } = require('comment-block-parser')
|
|
2
|
+
|
|
3
|
+
const SYNTAX = 'md'
|
|
4
|
+
const OPEN_WORD = 'block'
|
|
5
|
+
const CLOSE_WORD = '/block'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Types from comment-block-parser
|
|
9
|
+
* @typedef {import('comment-block-parser').BlockData} BlockData
|
|
10
|
+
* @typedef {import('comment-block-parser').ParseBlocksResult} ParseBlocksResult
|
|
11
|
+
* @typedef {import('comment-block-parser').BlockDetails} BlockDetails
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Key value pair of transform name to transform function
|
|
16
|
+
* @typedef {Record<string, TransformFunction>} TransformerPlugins
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Transform function signature
|
|
21
|
+
* @typedef {(api: TransformApi) => Promise<string>|string} TransformFunction
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Transform function API.
|
|
26
|
+
* This is the object passed to the transform function.
|
|
27
|
+
* @typedef {Object} TransformApi
|
|
28
|
+
* @property {string} transform - Name of the transform
|
|
29
|
+
* @property {string} content - Content to transform
|
|
30
|
+
* @property {Object} options - Transform options
|
|
31
|
+
* @property {string} [srcPath] - Source file path
|
|
32
|
+
* @property {string} [outputPath] - Output file path
|
|
33
|
+
* @property {{regex: Object, [key: string]: any}} settings - Additional settings including regex patterns
|
|
34
|
+
* @property {string} currentContent - Current file contents
|
|
35
|
+
* @property {string} originalContent - Original file contents
|
|
36
|
+
* @property {() => string} getCurrentContent - Function to get current file contents
|
|
37
|
+
* @property {() => string} getOriginalContent - Function to get original file contents
|
|
38
|
+
* @property {() => BlockContext} getOriginalBlock - Function to get the original block data
|
|
39
|
+
* @property {(content?: string) => Object} getBlockDetails - Function to get detailed block information
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extended block context with additional metadata
|
|
44
|
+
* @typedef {BlockData & {
|
|
45
|
+
* srcPath?: string,
|
|
46
|
+
* regex: {blocks: RegExp, open: RegExp, close: RegExp},
|
|
47
|
+
* originalContents: string,
|
|
48
|
+
* currentContents: string
|
|
49
|
+
* }} BlockContext
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Middleware function signature
|
|
54
|
+
* @typedef {Object} Middleware
|
|
55
|
+
* @property {string} name - Name of the middleware
|
|
56
|
+
* @property {function(BlockData, string): Promise<string>|string} transform - Transform function that takes block data and current text and returns transformed content
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Configuration object for processing contents.
|
|
61
|
+
* @typedef {Object} ProcessContentConfig
|
|
62
|
+
* @property {string} [open=OPEN_WORD] - The opening delimiter.
|
|
63
|
+
* @property {string} [close=CLOSE_WORD] - The closing delimiter.
|
|
64
|
+
* @property {string} [syntax='md'] - The syntax type.
|
|
65
|
+
* @property {TransformerPlugins} [transforms={}] - Plugins for transforms.
|
|
66
|
+
* @property {Array<Middleware>} [beforeMiddleware=[]] - Middleware functions change inner block content before transforms.
|
|
67
|
+
* @property {Array<Middleware>} [afterMiddleware=[]] - Middleware functions change inner block content after transforms.
|
|
68
|
+
* @property {boolean} [removeComments=false] - Remove comments from the processed contents.
|
|
69
|
+
* @property {string} [srcPath] - The source path.
|
|
70
|
+
* @property {string} [outputPath] - The output path.
|
|
71
|
+
* @property {import('comment-block-parser').CustomPatterns} [customPatterns] - Custom regex patterns for open and close tags.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {Object} BlockTransformerResult
|
|
76
|
+
* @property {boolean} isChanged - Whether the content was changed by transforms
|
|
77
|
+
* @property {boolean} isNewPath - Whether srcPath differs from outputPath
|
|
78
|
+
* @property {boolean} stripComments - Whether to strip comments from output
|
|
79
|
+
* @property {string} [srcPath] - Source file path
|
|
80
|
+
* @property {string} [outputPath] - Output file path
|
|
81
|
+
* @property {Array<BlockData>} transforms - Array of transforms that were applied
|
|
82
|
+
* @property {Array<any>} missingTransforms - Array of transforms that were not found
|
|
83
|
+
* @property {string} originalContents - Original input text
|
|
84
|
+
* @property {string} updatedContents - Transformed output text
|
|
85
|
+
* @property {Object} [patterns] - Regex patterns used for parsing
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Transform markdown blocks based on configured transforms
|
|
90
|
+
* @param {string} inputText - The text content to process
|
|
91
|
+
* @param {ProcessContentConfig} config - Configuration options
|
|
92
|
+
* @returns {Promise<BlockTransformerResult>} Result object containing transformed content and metadata
|
|
93
|
+
*/
|
|
94
|
+
async function blockTransformer(inputText, config) {
|
|
95
|
+
const opts = config || {}
|
|
96
|
+
|
|
97
|
+
const {
|
|
98
|
+
srcPath,
|
|
99
|
+
outputPath,
|
|
100
|
+
open = OPEN_WORD,
|
|
101
|
+
close = CLOSE_WORD,
|
|
102
|
+
syntax = SYNTAX,
|
|
103
|
+
transforms = {},
|
|
104
|
+
beforeMiddleware = [],
|
|
105
|
+
afterMiddleware = [],
|
|
106
|
+
removeComments = false,
|
|
107
|
+
customPatterns
|
|
108
|
+
} = opts
|
|
109
|
+
|
|
110
|
+
let foundBlocks = {}
|
|
111
|
+
try {
|
|
112
|
+
foundBlocks = parseBlocks(inputText, {
|
|
113
|
+
syntax,
|
|
114
|
+
open,
|
|
115
|
+
close,
|
|
116
|
+
customPatterns
|
|
117
|
+
})
|
|
118
|
+
} catch (e) {
|
|
119
|
+
const errMsg = (srcPath) ? `in ${srcPath}` : inputText
|
|
120
|
+
throw new Error(`${e.message}\nFix content in ${errMsg}\n`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
const { COMMENT_OPEN_REGEX, COMMENT_CLOSE_REGEX } = foundBlocks
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
const blocksWithTransforms = foundBlocks.blocks
|
|
128
|
+
.filter((block) => block.type)
|
|
129
|
+
.map((block, i) => {
|
|
130
|
+
const transform = block.type
|
|
131
|
+
delete block.type
|
|
132
|
+
return Object.assign({ transform }, block)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// console.log('blocksWithTransforms', blocksWithTransforms)
|
|
136
|
+
|
|
137
|
+
const regexInfo = {
|
|
138
|
+
blocks: foundBlocks.pattern,
|
|
139
|
+
open: COMMENT_OPEN_REGEX,
|
|
140
|
+
close: COMMENT_CLOSE_REGEX,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const transformsToRun = sortTransforms(blocksWithTransforms, transforms)
|
|
144
|
+
|
|
145
|
+
let missingTransforms = []
|
|
146
|
+
let updatedContents = await transformsToRun.reduce(async (contentPromise, originalMatch) => {
|
|
147
|
+
const updatedText = await contentPromise
|
|
148
|
+
/* Apply leading middleware */
|
|
149
|
+
const match = await applyMiddleware(originalMatch, updatedText, beforeMiddleware)
|
|
150
|
+
const { block, content, open, close, transform, options, context, index } = match
|
|
151
|
+
const closeTag = close.value
|
|
152
|
+
const openTag = open.value
|
|
153
|
+
|
|
154
|
+
let tempContent = content.value
|
|
155
|
+
const currentTransformFn = getTransform(transform, transforms)
|
|
156
|
+
|
|
157
|
+
if (currentTransformFn) {
|
|
158
|
+
const blockContext = {
|
|
159
|
+
srcPath: config.srcPath,
|
|
160
|
+
...match,
|
|
161
|
+
regex: regexInfo,
|
|
162
|
+
originalContents: inputText,
|
|
163
|
+
currentContents: updatedText,
|
|
164
|
+
}
|
|
165
|
+
const { transforms, srcPath, outputPath, ...restOfSettings } = opts
|
|
166
|
+
|
|
167
|
+
const returnedContent = await currentTransformFn({
|
|
168
|
+
transform, // transform name
|
|
169
|
+
content: content.value,
|
|
170
|
+
options: options || {},
|
|
171
|
+
srcPath,
|
|
172
|
+
outputPath,
|
|
173
|
+
settings: {
|
|
174
|
+
...restOfSettings,
|
|
175
|
+
regex: blockContext.regex,
|
|
176
|
+
},
|
|
177
|
+
currentContent: updatedText,
|
|
178
|
+
originalContent: inputText,
|
|
179
|
+
getCurrentContent: () => updatedText,
|
|
180
|
+
getOriginalContent: () => inputText,
|
|
181
|
+
getOriginalBlock: () => blockContext,
|
|
182
|
+
getBlockDetails: (content) => {
|
|
183
|
+
return getDetails({
|
|
184
|
+
contents: content || updatedText,
|
|
185
|
+
openValue: open.value,
|
|
186
|
+
srcPath,
|
|
187
|
+
index,
|
|
188
|
+
opts: config
|
|
189
|
+
})
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
if (returnedContent) {
|
|
194
|
+
tempContent = returnedContent
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Apply trailing middleware */
|
|
199
|
+
const afterContent = await applyMiddleware({
|
|
200
|
+
...match,
|
|
201
|
+
...{
|
|
202
|
+
content: {
|
|
203
|
+
...match.content,
|
|
204
|
+
value: tempContent
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}, updatedText, afterMiddleware)
|
|
208
|
+
|
|
209
|
+
if (!currentTransformFn) {
|
|
210
|
+
missingTransforms.push(afterContent)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let newContent = afterContent.content.value
|
|
214
|
+
/* handle different cases of typeof newContent. @TODO: make this an option */
|
|
215
|
+
if (typeof newContent === 'number') {
|
|
216
|
+
newContent = String(newContent)
|
|
217
|
+
} else if (Array.isArray(newContent)) {
|
|
218
|
+
newContent = JSON.stringify(newContent, null, 2)
|
|
219
|
+
} else if (typeof newContent === 'object') {
|
|
220
|
+
newContent = JSON.stringify(newContent, null, 2)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const formattedNewContent = (options.noTrim) ? newContent : trimString(newContent)
|
|
224
|
+
const fix = removeConflictingComments(formattedNewContent, COMMENT_OPEN_REGEX, COMMENT_CLOSE_REGEX)
|
|
225
|
+
|
|
226
|
+
let preserveIndent = 0
|
|
227
|
+
if (match.content.indentation) {
|
|
228
|
+
preserveIndent = match.content.indentation.length
|
|
229
|
+
} else if (preserveIndent === 0) {
|
|
230
|
+
preserveIndent = block.indentation.length
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let addTrailingNewline = ''
|
|
234
|
+
if (context.isMultiline && !fix.endsWith('\n') && fix !== '' && closeTag.indexOf('\n') === -1) {
|
|
235
|
+
addTrailingNewline = '\n'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let addLeadingNewline = ''
|
|
239
|
+
if (context.isMultiline && !fix.startsWith('\n') && fix !== '' && openTag.indexOf('\n') === -1) {
|
|
240
|
+
addLeadingNewline = '\n'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let fixWrapper = ''
|
|
244
|
+
if (!context.isMultiline && fix.indexOf('\n') > -1) {
|
|
245
|
+
fixWrapper = '\n'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const indent = addLeadingNewline + indentString(fix, preserveIndent) + addTrailingNewline
|
|
249
|
+
const newCont = `${openTag}${fixWrapper}${indent}${fixWrapper}${closeTag}`
|
|
250
|
+
const newContents = updatedText.replace(block.value, () => newCont)
|
|
251
|
+
return Promise.resolve(newContents)
|
|
252
|
+
}, Promise.resolve(inputText))
|
|
253
|
+
|
|
254
|
+
const isNewPath = srcPath !== outputPath
|
|
255
|
+
|
|
256
|
+
if (removeComments && !isNewPath) {
|
|
257
|
+
throw new Error('"removeComments" can only be used if "outputPath" option is set. Otherwise this will break doc generation.')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const stripComments = isNewPath && removeComments
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
isChanged: inputText !== updatedContents,
|
|
264
|
+
isNewPath,
|
|
265
|
+
stripComments,
|
|
266
|
+
srcPath,
|
|
267
|
+
outputPath,
|
|
268
|
+
transforms: transformsToRun,
|
|
269
|
+
missingTransforms,
|
|
270
|
+
originalContents: inputText,
|
|
271
|
+
updatedContents,
|
|
272
|
+
patterns: regexInfo,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** @typedef {BlockData & { sourceLocation?: string, transform?: string }} BlockDataExtended */
|
|
277
|
+
|
|
278
|
+
function getDetails({
|
|
279
|
+
contents,
|
|
280
|
+
openValue,
|
|
281
|
+
srcPath,
|
|
282
|
+
index,
|
|
283
|
+
opts
|
|
284
|
+
}) {
|
|
285
|
+
const blockData = parseBlocks(contents, opts)
|
|
286
|
+
const matchingBlocks = blockData.blocks.filter((block) => {
|
|
287
|
+
return block.open.value === openValue
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
if (!matchingBlocks.length) {
|
|
291
|
+
return {}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @type {BlockDataExtended} */
|
|
295
|
+
let foundBlock = matchingBlocks[0]
|
|
296
|
+
if (matchingBlocks.length > 1 && index) {
|
|
297
|
+
foundBlock = matchingBlocks.filter((block) => {
|
|
298
|
+
return block.index === index
|
|
299
|
+
})[0]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (srcPath) {
|
|
303
|
+
const location = getCodeLocation(srcPath, foundBlock.block.lines[0])
|
|
304
|
+
foundBlock.sourceLocation = location
|
|
305
|
+
}
|
|
306
|
+
return foundBlock
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function removeConflictingComments(content, openPattern, closePattern) {
|
|
310
|
+
const removeOpen = content.replace(openPattern, '')
|
|
311
|
+
closePattern.lastIndex = 0
|
|
312
|
+
const hasClose = closePattern.exec(content)
|
|
313
|
+
if (!hasClose) {
|
|
314
|
+
return removeOpen
|
|
315
|
+
}
|
|
316
|
+
const closeTag = `${hasClose[2]}${hasClose[3] || ''}`
|
|
317
|
+
return removeOpen
|
|
318
|
+
.replace(closePattern, '')
|
|
319
|
+
.replace(/\n$/, '')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Apply middleware functions to block data
|
|
324
|
+
* @param {BlockData} data - The block data to transform
|
|
325
|
+
* @param {string} updatedText - The current updated text content
|
|
326
|
+
* @param {Array<Middleware>} middlewares - Array of middleware functions to apply
|
|
327
|
+
* @returns {Promise<BlockDataExtended>} The transformed block data
|
|
328
|
+
*/
|
|
329
|
+
function applyMiddleware(data, updatedText, middlewares) {
|
|
330
|
+
return middlewares.reduce(async (acc, curr) => {
|
|
331
|
+
const blockData = await acc
|
|
332
|
+
const updatedContent = await curr.transform(blockData, updatedText)
|
|
333
|
+
return Promise.resolve({
|
|
334
|
+
...blockData,
|
|
335
|
+
...{
|
|
336
|
+
content: {
|
|
337
|
+
...blockData.content,
|
|
338
|
+
value: updatedContent
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
}, Promise.resolve(data))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getTransform(name, transforms = {}) {
|
|
346
|
+
return transforms[name] || transforms[name.toLowerCase()]
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function sortTransforms(foundTransForms, registeredTransforms) {
|
|
350
|
+
if (!foundTransForms) return []
|
|
351
|
+
return foundTransForms.sort((a, b) => {
|
|
352
|
+
if (a.transform === 'TOC' || a.transform === 'sectionToc') return 1
|
|
353
|
+
if (b.transform === 'TOC' || b.transform === 'sectionToc') return -1
|
|
354
|
+
return 0
|
|
355
|
+
}).map((item) => {
|
|
356
|
+
if (getTransform(item.transform, registeredTransforms)) {
|
|
357
|
+
return item
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
...item,
|
|
361
|
+
context: {
|
|
362
|
+
...item.context,
|
|
363
|
+
isMissing: true,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Indent a string by a specified number of spaces
|
|
371
|
+
* @param {string} str - The string to indent
|
|
372
|
+
* @param {number} count - Number of spaces to indent by
|
|
373
|
+
* @returns {string} The indented string
|
|
374
|
+
*/
|
|
375
|
+
function indentString(str, count) {
|
|
376
|
+
if (!str) return str
|
|
377
|
+
return str.split('\n').map(line => ' '.repeat(count) + line).join('\n')
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Trim whitespace from a string
|
|
382
|
+
* @param {string} str - The string to trim
|
|
383
|
+
* @returns {string} The trimmed string
|
|
384
|
+
*/
|
|
385
|
+
function trimString(str) {
|
|
386
|
+
if (!str) return str
|
|
387
|
+
return str.trim()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getCodeLocation(srcPath, line, column = '0') {
|
|
391
|
+
return `${srcPath}:${line}:${column}`
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = {
|
|
395
|
+
blockTransformer
|
|
396
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { blockTransformer } = require('../src')
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../src').ProcessContentConfig} ProcessContentConfig */
|
|
6
|
+
|
|
7
|
+
// Mock some core transforms for testing
|
|
8
|
+
const mockCoreTransforms = {
|
|
9
|
+
TOC: (api) => {
|
|
10
|
+
return '# Table of Contents\n- [Example](#example)'
|
|
11
|
+
},
|
|
12
|
+
FILE: (api) => {
|
|
13
|
+
return `File content from ${api.options.src || 'unknown file'}`
|
|
14
|
+
},
|
|
15
|
+
CODE: (api) => {
|
|
16
|
+
return `\`\`\`\n// Code from ${api.options.src || 'unknown source'}\n\`\`\``
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('should transform markdown blocks', async () => {
|
|
21
|
+
const text = `
|
|
22
|
+
<!-- block test -->
|
|
23
|
+
Some content
|
|
24
|
+
<!-- /block -->
|
|
25
|
+
`
|
|
26
|
+
/** @type {ProcessContentConfig} */
|
|
27
|
+
const config = {
|
|
28
|
+
transforms: {
|
|
29
|
+
test: (api) => {
|
|
30
|
+
return api.content.toUpperCase()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = await blockTransformer(text, config)
|
|
36
|
+
// console.log('result', result)
|
|
37
|
+
assert.is(result.isChanged, true)
|
|
38
|
+
assert.ok(result.updatedContents.includes('SOME CONTENT'))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should handle missing transforms', async () => {
|
|
42
|
+
const text = `
|
|
43
|
+
<!-- block foobar -->
|
|
44
|
+
This will be transformed to uppercase
|
|
45
|
+
<!-- /block -->
|
|
46
|
+
`
|
|
47
|
+
/** @type {ProcessContentConfig} */
|
|
48
|
+
const config = {
|
|
49
|
+
transforms: {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await blockTransformer(text, config)
|
|
53
|
+
console.log('result', result)
|
|
54
|
+
assert.is(result.missingTransforms.length, 1)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('should apply middleware', async () => {
|
|
58
|
+
const text = `
|
|
59
|
+
<!-- block foobar -->
|
|
60
|
+
Some content
|
|
61
|
+
<!-- /block -->
|
|
62
|
+
`
|
|
63
|
+
const beforeMiddleware = [{
|
|
64
|
+
name: 'test',
|
|
65
|
+
transform: (blockData) => {
|
|
66
|
+
console.log('blockData', blockData)
|
|
67
|
+
return blockData.content.value.toUpperCase()
|
|
68
|
+
}
|
|
69
|
+
}]
|
|
70
|
+
/** @type {ProcessContentConfig} */
|
|
71
|
+
const config = {
|
|
72
|
+
transforms: {},
|
|
73
|
+
beforeMiddleware
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await blockTransformer(text, config)
|
|
77
|
+
assert.ok(result.updatedContents.includes('SOME CONTENT'))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('should handle custom delimiters', async () => {
|
|
81
|
+
const text = `
|
|
82
|
+
<!-- CUSTOM:START test -->
|
|
83
|
+
Some content
|
|
84
|
+
<!-- CUSTOM:END -->
|
|
85
|
+
`
|
|
86
|
+
/** @type {ProcessContentConfig} */
|
|
87
|
+
const config = {
|
|
88
|
+
open: 'CUSTOM:START',
|
|
89
|
+
close: 'CUSTOM:END',
|
|
90
|
+
transforms: {
|
|
91
|
+
test: (api) => {
|
|
92
|
+
return api.content.toUpperCase()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = await blockTransformer(text, config)
|
|
98
|
+
assert.is(result.isChanged, true)
|
|
99
|
+
assert.ok(result.updatedContents.includes('SOME CONTENT'))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('should handle multiple plugins', async () => {
|
|
103
|
+
const text = `
|
|
104
|
+
<!-- block upperCase -->
|
|
105
|
+
hello world
|
|
106
|
+
<!-- /block -->
|
|
107
|
+
|
|
108
|
+
<!-- block reverse -->
|
|
109
|
+
abc def
|
|
110
|
+
<!-- /block -->
|
|
111
|
+
|
|
112
|
+
<!-- block addPrefix -->
|
|
113
|
+
content here
|
|
114
|
+
<!-- /block -->
|
|
115
|
+
`
|
|
116
|
+
/** @type {ProcessContentConfig} */
|
|
117
|
+
const config = {
|
|
118
|
+
transforms: {
|
|
119
|
+
upperCase: (api) => {
|
|
120
|
+
return api.content.toUpperCase()
|
|
121
|
+
},
|
|
122
|
+
reverse: (api) => {
|
|
123
|
+
return api.content.split('').reverse().join('')
|
|
124
|
+
},
|
|
125
|
+
addPrefix: (api) => {
|
|
126
|
+
return `PREFIX: ${api.content}`
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result = await blockTransformer(text, config)
|
|
132
|
+
assert.is(result.isChanged, true)
|
|
133
|
+
assert.ok(result.updatedContents.includes('HELLO WORLD'))
|
|
134
|
+
assert.ok(result.updatedContents.includes('fed cba'))
|
|
135
|
+
assert.ok(result.updatedContents.includes('PREFIX: content here'))
|
|
136
|
+
assert.is(result.transforms.length, 3)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('should handle multiple before middlewares', async () => {
|
|
140
|
+
const text = `
|
|
141
|
+
<!-- block test -->
|
|
142
|
+
hello world
|
|
143
|
+
<!-- /block -->
|
|
144
|
+
`
|
|
145
|
+
const beforeMiddleware = [
|
|
146
|
+
{
|
|
147
|
+
name: 'upperCase',
|
|
148
|
+
transform: (blockData) => {
|
|
149
|
+
return blockData.content.value.toUpperCase()
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'addExclamation',
|
|
154
|
+
transform: (blockData) => {
|
|
155
|
+
return blockData.content.value + '!'
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'addPrefix',
|
|
160
|
+
transform: (blockData) => {
|
|
161
|
+
return `PREFIX: ${blockData.content.value}`
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
/** @type {ProcessContentConfig} */
|
|
166
|
+
const config = {
|
|
167
|
+
transforms: {
|
|
168
|
+
test: (api) => {
|
|
169
|
+
return api.content
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
beforeMiddleware
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = await blockTransformer(text, config)
|
|
176
|
+
assert.is(result.isChanged, true)
|
|
177
|
+
assert.ok(result.updatedContents.includes('PREFIX: HELLO WORLD!'))
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('should handle multiple after middlewares', async () => {
|
|
181
|
+
const text = `
|
|
182
|
+
<!-- block test -->
|
|
183
|
+
hello world
|
|
184
|
+
<!-- /block -->
|
|
185
|
+
`
|
|
186
|
+
const afterMiddleware = [
|
|
187
|
+
{
|
|
188
|
+
name: 'upperCase',
|
|
189
|
+
transform: (blockData) => {
|
|
190
|
+
return blockData.content.value.toUpperCase()
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'addSuffix',
|
|
195
|
+
transform: (blockData) => {
|
|
196
|
+
return `${blockData.content.value} - PROCESSED`
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'wrapBrackets',
|
|
201
|
+
transform: (blockData) => {
|
|
202
|
+
return `[${blockData.content.value}]`
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
/** @type {ProcessContentConfig} */
|
|
207
|
+
const config = {
|
|
208
|
+
transforms: {
|
|
209
|
+
test: (api) => {
|
|
210
|
+
return api.content.trim()
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
afterMiddleware
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const result = await blockTransformer(text, config)
|
|
217
|
+
assert.is(result.isChanged, true)
|
|
218
|
+
assert.ok(result.updatedContents.includes('[HELLO WORLD - PROCESSED]'))
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('should handle both before and after middlewares together', async () => {
|
|
222
|
+
const text = `
|
|
223
|
+
<!-- block test -->
|
|
224
|
+
content
|
|
225
|
+
<!-- /block -->
|
|
226
|
+
`
|
|
227
|
+
const beforeMiddleware = [
|
|
228
|
+
{
|
|
229
|
+
name: 'addBefore',
|
|
230
|
+
transform: (blockData) => {
|
|
231
|
+
return `BEFORE_${blockData.content.value}`
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
const afterMiddleware = [
|
|
236
|
+
{
|
|
237
|
+
name: 'addAfter',
|
|
238
|
+
transform: (blockData) => {
|
|
239
|
+
return `${blockData.content.value}_AFTER`
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
/** @type {ProcessContentConfig} */
|
|
244
|
+
const config = {
|
|
245
|
+
transforms: {
|
|
246
|
+
test: (api) => {
|
|
247
|
+
return `_${api.content.toUpperCase()}_`
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
beforeMiddleware,
|
|
251
|
+
afterMiddleware
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result = await blockTransformer(text, config)
|
|
255
|
+
assert.is(result.isChanged, true)
|
|
256
|
+
assert.ok(result.updatedContents.includes('_BEFORE_CONTENT__AFTER'))
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('should handle multiple plugins with core transforms', async () => {
|
|
260
|
+
const text = `
|
|
261
|
+
<!-- block upperCase -->
|
|
262
|
+
hello world
|
|
263
|
+
<!-- /block -->
|
|
264
|
+
|
|
265
|
+
<!-- block TOC -->
|
|
266
|
+
<!-- /block -->
|
|
267
|
+
`
|
|
268
|
+
/** @type {ProcessContentConfig} */
|
|
269
|
+
const config = {
|
|
270
|
+
transforms: {
|
|
271
|
+
upperCase: (api) => {
|
|
272
|
+
return api.content.toUpperCase()
|
|
273
|
+
},
|
|
274
|
+
...mockCoreTransforms
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result = await blockTransformer(text, config)
|
|
279
|
+
assert.is(result.isChanged, true)
|
|
280
|
+
assert.ok(result.updatedContents.includes('HELLO WORLD'))
|
|
281
|
+
assert.ok(result.updatedContents.includes('Table of Contents'))
|
|
282
|
+
assert.is(result.transforms.length, 2)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('should apply middlewares to multiple blocks independently', async () => {
|
|
286
|
+
const text = `
|
|
287
|
+
<!-- block block1 -->
|
|
288
|
+
first block
|
|
289
|
+
<!-- /block -->
|
|
290
|
+
|
|
291
|
+
<!-- block block2 -->
|
|
292
|
+
second block
|
|
293
|
+
<!-- /block -->
|
|
294
|
+
`
|
|
295
|
+
const beforeMiddleware = [
|
|
296
|
+
{
|
|
297
|
+
name: 'addIndex',
|
|
298
|
+
transform: (blockData) => {
|
|
299
|
+
const blockIndex = blockData.transform === 'block1' ? '1' : '2'
|
|
300
|
+
return `Block ${blockIndex}: ${blockData.content.value}`
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
/** @type {ProcessContentConfig} */
|
|
305
|
+
const config = {
|
|
306
|
+
transforms: {
|
|
307
|
+
block1: (api) => api.content.toUpperCase(),
|
|
308
|
+
block2: (api) => api.content.toLowerCase()
|
|
309
|
+
},
|
|
310
|
+
beforeMiddleware
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const result = await blockTransformer(text, config)
|
|
314
|
+
assert.is(result.isChanged, true)
|
|
315
|
+
assert.ok(result.updatedContents.includes('BLOCK 1: FIRST BLOCK'))
|
|
316
|
+
assert.ok(result.updatedContents.includes('block 2: second block'))
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test.run()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const util = require('util')
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function logValue(value, isFirst, isLast) {
|
|
5
|
+
const prefix = `${isFirst ? '> ' : ''}`
|
|
6
|
+
if (typeof value === 'object') {
|
|
7
|
+
console.log(`${util.inspect(value, false, null, true)}\n`)
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
if (isFirst) {
|
|
11
|
+
console.log(`\n\x1b[33m${prefix}${value}\x1b[0m`)
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
console.log((typeof value === 'string' && value.includes('\n')) ? `\`${value}\`` : value)
|
|
15
|
+
// isLast && console.log(`\x1b[37m\x1b[1m${'─'.repeat(94)}\x1b[0m\n`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = function deepLog() {
|
|
19
|
+
for (let i = 0; i < arguments.length; i++) logValue(arguments[i], i === 0, i === arguments.length - 1)
|
|
20
|
+
}
|