telegram-md2html 1.0.0 → 1.0.2

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/dist/index.cjs CHANGED
@@ -293,6 +293,8 @@ class MarkdownConverter {
293
293
  }
294
294
  /**
295
295
  * Wrap token content in HTML tags
296
+ * FIXED: Removed extra newlines that were being added around code blocks and quotes
297
+ * Previously added \n before and after, now returns clean tags without extra whitespace
296
298
  */
297
299
  wrapToken(type, content, language) {
298
300
  switch (type) {
@@ -316,7 +318,8 @@ class MarkdownConverter {
316
318
  }
317
319
  const escapedCode = this.options.escapeHtml ? escapeHtml(content) : content;
318
320
  const langAttr = language ? ` class="language-${language}"` : '';
319
- return `\n<pre><code${langAttr}>${escapedCode}</code></pre>\n`;
321
+ // FIXED: Removed \n before and after - now returns just the tag
322
+ return `<pre><code${langAttr}>${escapedCode}</code></pre>`;
320
323
  case 'link':
321
324
  const url = language || '';
322
325
  if (this.hasCustomLinkProcessor) {
@@ -326,9 +329,11 @@ class MarkdownConverter {
326
329
  const escapedText = this.options.escapeHtml ? escapeHtml(content) : content;
327
330
  return `<a href="${escapedUrl}">${escapedText}</a>`;
328
331
  case 'quote':
329
- return `\n<blockquote>${content.trim()}</blockquote>\n`;
332
+ // FIXED: Removed \n before and after - now returns just the tag
333
+ return `<blockquote>${content.trim()}</blockquote>`;
330
334
  case 'expandable_quote':
331
- return `\n<blockquote expandable>${content.trim()}</blockquote>\n`;
335
+ // FIXED: Removed \n before and after - now returns just the tag
336
+ return `<blockquote expandable>${content.trim()}</blockquote>`;
332
337
  default:
333
338
  return content;
334
339
  }
@@ -360,6 +365,7 @@ class MarkdownConverter {
360
365
  }
361
366
  /**
362
367
  * Process blockquote markers
368
+ * FIXED: Removed extra newlines from the replacement strings
363
369
  */
364
370
  processBlockquoteMarkers(text) {
365
371
  let result = text;
@@ -367,13 +373,15 @@ class MarkdownConverter {
367
373
  const expandableQuoteRegex = /\[EXPANDABLE_QUOTE\](.*?)(?=\n|$)/g;
368
374
  result = result.replace(expandableQuoteRegex, (match, content) => {
369
375
  const processedContent = this.convertRecursive(content);
370
- return `\n<blockquote expandable>${processedContent.trim()}</blockquote>\n`;
376
+ // FIXED: Removed \n before and after
377
+ return `<blockquote expandable>${processedContent.trim()}</blockquote>`;
371
378
  });
372
379
  // Replace regular quote markers (process content recursively)
373
380
  const quoteRegex = /\[QUOTE\](.*?)(?=\n|$)/g;
374
381
  result = result.replace(quoteRegex, (match, content) => {
375
382
  const processedContent = this.convertRecursive(content);
376
- return `\n<blockquote>${processedContent.trim()}</blockquote>\n`;
383
+ // FIXED: Removed \n before and after
384
+ return `<blockquote>${processedContent.trim()}</blockquote>`;
377
385
  });
378
386
  return result;
379
387
  }
@@ -385,7 +393,8 @@ class MarkdownConverter {
385
393
  defaultCodeBlockProcessor(code, language) {
386
394
  const escapedCode = this.options.escapeHtml ? escapeHtml(code) : code;
387
395
  const langAttr = language ? ` class="language-${language}"` : '';
388
- return `\n<pre><code${langAttr}>${escapedCode}</code></pre>\n`;
396
+ // FIXED: Removed \n before and after in default processor too
397
+ return `<pre><code${langAttr}>${escapedCode}</code></pre>`;
389
398
  }
390
399
  }
391
400
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MarkdownConverter } from './converter.js';
2
- import { ConvertOptions } from './types.js';
2
+ import type { ConvertOptions } from './types.js';
3
3
  /**
4
4
  * Convert Telegram-style Markdown to HTML
5
5
  * @param text - Markdown text to convert
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MarkdownConverter } from './converter.js';
2
- import { ConvertOptions } from './types.js';
2
+ import type { ConvertOptions } from './types.js';
3
3
  /**
4
4
  * Convert Telegram-style Markdown to HTML
5
5
  * @param text - Markdown text to convert
@@ -0,0 +1,419 @@
1
+ class MarkdownTokenizer {
2
+ constructor(text) {
3
+ this.text = text;
4
+ }
5
+ /**
6
+ * Tokenize the markdown text
7
+ */
8
+ tokenize() {
9
+ const tokens = [];
10
+ let pos = 0;
11
+ const text = this.text;
12
+ while (pos < text.length) {
13
+ // Skip if inside code block
14
+ if (this.isInsideCodeBlock(text, pos)) {
15
+ pos++;
16
+ continue;
17
+ }
18
+ // Try to match each token type (from outermost to innermost)
19
+ const token = this.matchToken(pos);
20
+ if (token) {
21
+ tokens.push(token);
22
+ pos = token.end;
23
+ }
24
+ else {
25
+ pos++;
26
+ }
27
+ }
28
+ return tokens.sort((a, b) => a.start - b.start);
29
+ }
30
+ matchToken(start) {
31
+ const text = this.text;
32
+ const remaining = text.slice(start);
33
+ // Skip if we're inside a quote marker
34
+ if (remaining.startsWith('[QUOTE]') || remaining.startsWith('[EXPANDABLE_QUOTE]')) {
35
+ return null;
36
+ }
37
+ // Match code block (triple backticks) - highest priority
38
+ const codeBlockMatch = remaining.match(/^```(\w+)?\n([\s\S]*?)```/);
39
+ if (codeBlockMatch) {
40
+ return {
41
+ type: 'code_block',
42
+ content: codeBlockMatch[2],
43
+ language: codeBlockMatch[1],
44
+ start: start,
45
+ end: start + codeBlockMatch[0].length
46
+ };
47
+ }
48
+ // Match inline code
49
+ const inlineCodeMatch = remaining.match(/^`([^`\n]+)`/);
50
+ if (inlineCodeMatch && !this.isInsideInlineCode(text, start)) {
51
+ return {
52
+ type: 'inline_code',
53
+ content: inlineCodeMatch[1],
54
+ start: start,
55
+ end: start + inlineCodeMatch[0].length
56
+ };
57
+ }
58
+ // Match spoiler
59
+ const spoilerMatch = remaining.match(/^\|\|([^|\n]+?)\|\|/);
60
+ if (spoilerMatch) {
61
+ return {
62
+ type: 'spoiler',
63
+ content: spoilerMatch[1],
64
+ start: start,
65
+ end: start + spoilerMatch[0].length
66
+ };
67
+ }
68
+ // Match strikethrough
69
+ const strikethroughMatch = remaining.match(/^~~([^~\n]+?)~~/);
70
+ if (strikethroughMatch) {
71
+ return {
72
+ type: 'strikethrough',
73
+ content: strikethroughMatch[1],
74
+ start: start,
75
+ end: start + strikethroughMatch[0].length
76
+ };
77
+ }
78
+ // Match bold
79
+ const boldMatch = remaining.match(/^\*\*([^*\n]+?)\*\*/);
80
+ if (boldMatch) {
81
+ return {
82
+ type: 'bold',
83
+ content: boldMatch[1],
84
+ start: start,
85
+ end: start + boldMatch[0].length
86
+ };
87
+ }
88
+ // Match underline
89
+ const underlineMatch = remaining.match(/^__([^_\n]+?)__/);
90
+ if (underlineMatch) {
91
+ return {
92
+ type: 'underline',
93
+ content: underlineMatch[1],
94
+ start: start,
95
+ end: start + underlineMatch[0].length
96
+ };
97
+ }
98
+ // Match italic with asterisk
99
+ const italicAsteriskMatch = remaining.match(/^\*([^*\n][^*]*?)\*/);
100
+ if (italicAsteriskMatch && italicAsteriskMatch[1].trim().length > 0) {
101
+ // Don't match if it's part of bold (**)
102
+ if (start > 0 && text[start - 1] === '*' && start < text.length - 1 && text[start + 1] === '*') {
103
+ return null;
104
+ }
105
+ return {
106
+ type: 'italic',
107
+ content: italicAsteriskMatch[1],
108
+ start: start,
109
+ end: start + italicAsteriskMatch[0].length
110
+ };
111
+ }
112
+ // Match italic with underscore
113
+ const italicUnderscoreMatch = remaining.match(/^_([^_\n]+?)_/);
114
+ if (italicUnderscoreMatch && italicUnderscoreMatch[1].trim().length > 0) {
115
+ // Don't match if it's part of underline (__)
116
+ if (start > 0 && text[start - 1] === '_' && start < text.length - 1 && text[start + 1] === '_') {
117
+ return null;
118
+ }
119
+ return {
120
+ type: 'italic',
121
+ content: italicUnderscoreMatch[1],
122
+ start: start,
123
+ end: start + italicUnderscoreMatch[0].length
124
+ };
125
+ }
126
+ // Match link
127
+ const linkMatch = remaining.match(/^\[([^\]]+?)\]\(([^)]+?)\)/);
128
+ if (linkMatch) {
129
+ return {
130
+ type: 'link',
131
+ content: linkMatch[1],
132
+ start: start,
133
+ end: start + linkMatch[0].length,
134
+ language: linkMatch[2]
135
+ };
136
+ }
137
+ return null;
138
+ }
139
+ isInsideCodeBlock(text, position) {
140
+ // Check for code blocks
141
+ const codeBlockRegex = /```[\s\S]*?```/g;
142
+ let match;
143
+ while ((match = codeBlockRegex.exec(text)) !== null) {
144
+ if (position > match.index && position < match.index + match[0].length) {
145
+ // But allow matching the closing ``` itself
146
+ if (position >= match.index + match[0].length - 3) {
147
+ return false;
148
+ }
149
+ return true;
150
+ }
151
+ }
152
+ return false;
153
+ }
154
+ isInsideInlineCode(text, position) {
155
+ // Check for inline code
156
+ const inlineCodeRegex = /`[^`\n]*`/g;
157
+ let match;
158
+ while ((match = inlineCodeRegex.exec(text)) !== null) {
159
+ if (position > match.index && position < match.index + match[0].length) {
160
+ // But allow matching the closing ` itself
161
+ if (position === match.index + match[0].length - 1) {
162
+ return false;
163
+ }
164
+ return true;
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Escapes HTML special characters (but not double-escape)
173
+ */
174
+ function escapeHtml(text) {
175
+ if (!text)
176
+ return text;
177
+ // Replace & first (but not if it's already an entity)
178
+ let result = text.replace(/&(?!#?\w+;)/g, '&amp;');
179
+ result = result.replace(/</g, '&lt;');
180
+ result = result.replace(/>/g, '&gt;');
181
+ result = result.replace(/"/g, '&quot;');
182
+ result = result.replace(/'/g, '&#39;');
183
+ return result;
184
+ }
185
+ /**
186
+ * Escapes Telegram HTML special characters
187
+ */
188
+ function escapeTelegramHtml(text) {
189
+ if (!text)
190
+ return text;
191
+ // For Telegram, we only need to escape &, <, >, and "
192
+ let result = text.replace(/&(?!#?\w+;)/g, '&amp;');
193
+ result = result.replace(/</g, '&lt;');
194
+ result = result.replace(/>/g, '&gt;');
195
+ result = result.replace(/"/g, '&quot;');
196
+ return result;
197
+ }
198
+ /**
199
+ * Appends missing code block delimiters
200
+ */
201
+ function autoCloseCodeBlocks(text) {
202
+ // Count triple backticks
203
+ const tripleBacktickCount = (text.match(/```/g) || []).length;
204
+ // If odd number, add closing backticks
205
+ if (tripleBacktickCount % 2 === 1) {
206
+ return text + '\n```';
207
+ }
208
+ return text;
209
+ }
210
+
211
+ class MarkdownConverter {
212
+ constructor(options = {}) {
213
+ this.hasCustomLinkProcessor = !!options.linkProcessor;
214
+ this.hasCustomCodeBlockProcessor = !!options.codeBlockProcessor;
215
+ this.options = {
216
+ escapeHtml: options.escapeHtml ?? true,
217
+ autoCloseCodeBlocks: options.autoCloseCodeBlocks ?? true,
218
+ linkProcessor: options.linkProcessor || this.defaultLinkProcessor.bind(this),
219
+ codeBlockProcessor: options.codeBlockProcessor || this.defaultCodeBlockProcessor.bind(this)
220
+ };
221
+ }
222
+ /**
223
+ * Convert markdown text to Telegram HTML
224
+ */
225
+ convert(text) {
226
+ // Auto-close code blocks if enabled
227
+ let processedText = this.options.autoCloseCodeBlocks
228
+ ? autoCloseCodeBlocks(text)
229
+ : text;
230
+ // First pass: convert blockquotes (they should be at line starts)
231
+ processedText = this.preprocessBlockquotes(processedText);
232
+ // Convert the text recursively
233
+ let result = this.convertRecursive(processedText);
234
+ // Process blockquote markers
235
+ result = this.processBlockquoteMarkers(result);
236
+ // Only trim if there's actual content (not just whitespace)
237
+ if (result.trim() === '') {
238
+ return text; // Return original text (spaces) if result is empty
239
+ }
240
+ return result.trim();
241
+ }
242
+ /**
243
+ * Recursively convert markdown, handling nested styles
244
+ */
245
+ convertRecursive(text, depth = 0) {
246
+ if (depth > 10)
247
+ return text; // Prevent infinite recursion
248
+ // Tokenize the text
249
+ const tokenizer = new MarkdownTokenizer(text);
250
+ const tokens = tokenizer.tokenize();
251
+ // If no tokens found, return the text as-is (with HTML escaping)
252
+ if (tokens.length === 0) {
253
+ return this.options.escapeHtml ? escapeTelegramHtml(text) : text;
254
+ }
255
+ let result = '';
256
+ let lastPos = 0;
257
+ for (const token of tokens) {
258
+ // Add text before token
259
+ if (token.start > lastPos) {
260
+ const textBefore = text.slice(lastPos, token.start);
261
+ result += this.options.escapeHtml ? escapeTelegramHtml(textBefore) : textBefore;
262
+ }
263
+ // Handle code blocks specially (no recursive parsing inside)
264
+ if (token.type === 'code_block') {
265
+ const codeContent = this.options.escapeHtml ? escapeHtml(token.content) : token.content;
266
+ result += this.wrapToken(token.type, codeContent, token.language);
267
+ lastPos = token.end;
268
+ continue;
269
+ }
270
+ // Handle inline code specially (no recursive parsing inside)
271
+ if (token.type === 'inline_code') {
272
+ const codeContent = this.options.escapeHtml ? escapeHtml(token.content) : token.content;
273
+ result += `<code>${codeContent}</code>`;
274
+ lastPos = token.end;
275
+ continue;
276
+ }
277
+ // Process other token content recursively
278
+ const tokenContent = this.convertRecursive(token.content, depth + 1);
279
+ // Wrap the content in appropriate HTML tags
280
+ result += this.wrapToken(token.type, tokenContent, token.language);
281
+ lastPos = token.end;
282
+ }
283
+ // Add remaining text
284
+ if (lastPos < text.length) {
285
+ const remainingText = text.slice(lastPos);
286
+ result += this.options.escapeHtml ? escapeTelegramHtml(remainingText) : remainingText;
287
+ }
288
+ return result;
289
+ }
290
+ /**
291
+ * Wrap token content in HTML tags
292
+ * FIXED: Removed extra newlines that were being added around code blocks and quotes
293
+ * Previously added \n before and after, now returns clean tags without extra whitespace
294
+ */
295
+ wrapToken(type, content, language) {
296
+ switch (type) {
297
+ case 'bold':
298
+ return `<b>${content}</b>`;
299
+ case 'italic':
300
+ return `<i>${content}</i>`;
301
+ case 'underline':
302
+ return `<u>${content}</u>`;
303
+ case 'strikethrough':
304
+ return `<s>${content}</s>`;
305
+ case 'spoiler':
306
+ return `<span class="tg-spoiler">${content}</span>`;
307
+ case 'inline_code':
308
+ // Already handled above
309
+ return `<code>${content}</code>`;
310
+ case 'code_block':
311
+ // Already handled above, but handle custom processor
312
+ if (this.hasCustomCodeBlockProcessor) {
313
+ return this.options.codeBlockProcessor(content, language);
314
+ }
315
+ const escapedCode = this.options.escapeHtml ? escapeHtml(content) : content;
316
+ const langAttr = language ? ` class="language-${language}"` : '';
317
+ // FIXED: Removed \n before and after - now returns just the tag
318
+ return `<pre><code${langAttr}>${escapedCode}</code></pre>`;
319
+ case 'link':
320
+ const url = language || '';
321
+ if (this.hasCustomLinkProcessor) {
322
+ return this.options.linkProcessor(url, content);
323
+ }
324
+ const escapedUrl = this.options.escapeHtml ? escapeHtml(url) : url;
325
+ const escapedText = this.options.escapeHtml ? escapeHtml(content) : content;
326
+ return `<a href="${escapedUrl}">${escapedText}</a>`;
327
+ case 'quote':
328
+ // FIXED: Removed \n before and after - now returns just the tag
329
+ return `<blockquote>${content.trim()}</blockquote>`;
330
+ case 'expandable_quote':
331
+ // FIXED: Removed \n before and after - now returns just the tag
332
+ return `<blockquote expandable>${content.trim()}</blockquote>`;
333
+ default:
334
+ return content;
335
+ }
336
+ }
337
+ /**
338
+ * Preprocess blockquotes to mark them before other parsing
339
+ */
340
+ preprocessBlockquotes(text) {
341
+ const lines = text.split('\n');
342
+ const processedLines = [];
343
+ for (const line of lines) {
344
+ const trimmedLine = line.trim();
345
+ // Only treat lines starting with > at the beginning of line as blockquotes
346
+ if (trimmedLine.startsWith('**>')) {
347
+ // Expandable blockquote
348
+ const content = trimmedLine.substring(3).trim();
349
+ processedLines.push(`[EXPANDABLE_QUOTE]${content}`);
350
+ }
351
+ else if (trimmedLine.startsWith('>')) {
352
+ // Regular blockquote
353
+ const content = trimmedLine.substring(1).trim();
354
+ processedLines.push(`[QUOTE]${content}`);
355
+ }
356
+ else {
357
+ processedLines.push(line);
358
+ }
359
+ }
360
+ return processedLines.join('\n');
361
+ }
362
+ /**
363
+ * Process blockquote markers
364
+ * FIXED: Removed extra newlines from the replacement strings
365
+ */
366
+ processBlockquoteMarkers(text) {
367
+ let result = text;
368
+ // Replace expandable quote markers (process content recursively)
369
+ const expandableQuoteRegex = /\[EXPANDABLE_QUOTE\](.*?)(?=\n|$)/g;
370
+ result = result.replace(expandableQuoteRegex, (match, content) => {
371
+ const processedContent = this.convertRecursive(content);
372
+ // FIXED: Removed \n before and after
373
+ return `<blockquote expandable>${processedContent.trim()}</blockquote>`;
374
+ });
375
+ // Replace regular quote markers (process content recursively)
376
+ const quoteRegex = /\[QUOTE\](.*?)(?=\n|$)/g;
377
+ result = result.replace(quoteRegex, (match, content) => {
378
+ const processedContent = this.convertRecursive(content);
379
+ // FIXED: Removed \n before and after
380
+ return `<blockquote>${processedContent.trim()}</blockquote>`;
381
+ });
382
+ return result;
383
+ }
384
+ defaultLinkProcessor(url, text) {
385
+ const escapedUrl = this.options.escapeHtml ? escapeHtml(url) : url;
386
+ const escapedText = this.options.escapeHtml ? escapeHtml(text) : text;
387
+ return `<a href="${escapedUrl}">${escapedText}</a>`;
388
+ }
389
+ defaultCodeBlockProcessor(code, language) {
390
+ const escapedCode = this.options.escapeHtml ? escapeHtml(code) : code;
391
+ const langAttr = language ? ` class="language-${language}"` : '';
392
+ // FIXED: Removed \n before and after in default processor too
393
+ return `<pre><code${langAttr}>${escapedCode}</code></pre>`;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Convert Telegram-style Markdown to HTML
399
+ * @param text - Markdown text to convert
400
+ * @param options - Conversion options
401
+ * @returns Telegram-compatible HTML
402
+ */
403
+ function markdownToHtml(text, options) {
404
+ const converter = new MarkdownConverter(options);
405
+ return converter.convert(text);
406
+ }
407
+ /**
408
+ * Create a converter instance with custom options
409
+ */
410
+ function createConverter(options) {
411
+ return new MarkdownConverter(options);
412
+ }
413
+ var index = {
414
+ markdownToHtml,
415
+ createConverter,
416
+ MarkdownConverter
417
+ };
418
+
419
+ export { MarkdownConverter, createConverter, index as default, markdownToHtml };
package/dist/index.mjs CHANGED
@@ -289,6 +289,8 @@ class MarkdownConverter {
289
289
  }
290
290
  /**
291
291
  * Wrap token content in HTML tags
292
+ * FIXED: Removed extra newlines that were being added around code blocks and quotes
293
+ * Previously added \n before and after, now returns clean tags without extra whitespace
292
294
  */
293
295
  wrapToken(type, content, language) {
294
296
  switch (type) {
@@ -312,7 +314,8 @@ class MarkdownConverter {
312
314
  }
313
315
  const escapedCode = this.options.escapeHtml ? escapeHtml(content) : content;
314
316
  const langAttr = language ? ` class="language-${language}"` : '';
315
- return `\n<pre><code${langAttr}>${escapedCode}</code></pre>\n`;
317
+ // FIXED: Removed \n before and after - now returns just the tag
318
+ return `<pre><code${langAttr}>${escapedCode}</code></pre>`;
316
319
  case 'link':
317
320
  const url = language || '';
318
321
  if (this.hasCustomLinkProcessor) {
@@ -322,9 +325,11 @@ class MarkdownConverter {
322
325
  const escapedText = this.options.escapeHtml ? escapeHtml(content) : content;
323
326
  return `<a href="${escapedUrl}">${escapedText}</a>`;
324
327
  case 'quote':
325
- return `\n<blockquote>${content.trim()}</blockquote>\n`;
328
+ // FIXED: Removed \n before and after - now returns just the tag
329
+ return `<blockquote>${content.trim()}</blockquote>`;
326
330
  case 'expandable_quote':
327
- return `\n<blockquote expandable>${content.trim()}</blockquote>\n`;
331
+ // FIXED: Removed \n before and after - now returns just the tag
332
+ return `<blockquote expandable>${content.trim()}</blockquote>`;
328
333
  default:
329
334
  return content;
330
335
  }
@@ -356,6 +361,7 @@ class MarkdownConverter {
356
361
  }
357
362
  /**
358
363
  * Process blockquote markers
364
+ * FIXED: Removed extra newlines from the replacement strings
359
365
  */
360
366
  processBlockquoteMarkers(text) {
361
367
  let result = text;
@@ -363,13 +369,15 @@ class MarkdownConverter {
363
369
  const expandableQuoteRegex = /\[EXPANDABLE_QUOTE\](.*?)(?=\n|$)/g;
364
370
  result = result.replace(expandableQuoteRegex, (match, content) => {
365
371
  const processedContent = this.convertRecursive(content);
366
- return `\n<blockquote expandable>${processedContent.trim()}</blockquote>\n`;
372
+ // FIXED: Removed \n before and after
373
+ return `<blockquote expandable>${processedContent.trim()}</blockquote>`;
367
374
  });
368
375
  // Replace regular quote markers (process content recursively)
369
376
  const quoteRegex = /\[QUOTE\](.*?)(?=\n|$)/g;
370
377
  result = result.replace(quoteRegex, (match, content) => {
371
378
  const processedContent = this.convertRecursive(content);
372
- return `\n<blockquote>${processedContent.trim()}</blockquote>\n`;
379
+ // FIXED: Removed \n before and after
380
+ return `<blockquote>${processedContent.trim()}</blockquote>`;
373
381
  });
374
382
  return result;
375
383
  }
@@ -381,7 +389,8 @@ class MarkdownConverter {
381
389
  defaultCodeBlockProcessor(code, language) {
382
390
  const escapedCode = this.options.escapeHtml ? escapeHtml(code) : code;
383
391
  const langAttr = language ? ` class="language-${language}"` : '';
384
- return `\n<pre><code${langAttr}>${escapedCode}</code></pre>\n`;
392
+ // FIXED: Removed \n before and after in default processor too
393
+ return `<pre><code${langAttr}>${escapedCode}</code></pre>`;
385
394
  }
386
395
  }
387
396