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 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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "noEmit": false,
6
+ "declarationDir": "./types"
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules", "dist", "test"]
10
+ }