• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hexojs / hexo / 18879282237

28 Oct 2025 03:02PM UTC coverage: 99.489% (-0.04%) from 99.529%
18879282237

Pull #5717

github

web-flow
Merge 029b1031b into 69f358bb9
Pull Request #5717: fix(regression): handle comment nesting in code fence and nunjucks

2416 of 2513 branches covered (96.14%)

44 of 46 new or added lines in 2 files covered. (95.65%)

2 existing lines in 1 file now uncovered.

9938 of 9989 relevant lines covered (99.49%)

54.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.48
/lib/hexo/post.ts
1
import assert from 'assert';
1✔
2
import moment from 'moment';
1✔
3
import Promise from 'bluebird';
1✔
4
import { join, extname, basename } from 'path';
1✔
5
import { magenta } from 'picocolors';
1✔
6
import { load } from 'js-yaml';
1✔
7
import { slugize, escapeRegExp, deepMerge} from 'hexo-util';
1✔
8
import { copyDir, exists, listDir, mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs';
1✔
9
import { parse as yfmParse, split as yfmSplit, stringify as yfmStringify } from 'hexo-front-matter';
1✔
10
import type Hexo from './index';
1✔
11
import type { NodeJSLikeCallback, RenderData } from '../types';
1✔
12

1✔
13
const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content'];
1✔
14

1✔
15
const rHexoPostRenderEscape = /<hexoPostRenderCodeBlock>([\s\S]+?)<\/hexoPostRenderCodeBlock>/g;
1✔
16
const rCommentEscape = /(<!--[\s\S]*?-->)/g;
1✔
17
const rSwigTag = /(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/s;
1✔
18

1✔
19
const rSwigPlaceHolder = /(?:<|&lt;)!--swig\uFFFC(\d+)--(?:>|&gt;)/g;
1✔
20
const rCodeBlockPlaceHolder = /(?:<|&lt;)!--code\uFFFC(\d+)--(?:>|&gt;)/g;
1✔
21
const rCommentHolder = /(?:<|&lt;)!--comment\uFFFC(\d+)--(?:>|&gt;)/g;
1✔
22

1✔
23
const STATE_PLAINTEXT = 0;
1✔
24
const STATE_SWIG_VAR = 1;
1✔
25
const STATE_SWIG_COMMENT = 2;
1✔
26
const STATE_SWIG_TAG = 3;
1✔
27
const STATE_SWIG_FULL_TAG = 4;
1✔
28
const STATE_PLAINTEXT_COMMENT = 5;
1✔
29

1✔
30
const isNonWhiteSpaceChar = (char: string) => char !== '\r'
1✔
31
  && char !== '\n'
726✔
32
  && char !== '\t'
726✔
33
  && char !== '\f'
726✔
34
  && char !== '\v'
726✔
35
  && char !== ' ';
1✔
36

1✔
37
class PostRenderEscape {
1✔
38
  public stored: string[];
1✔
39
  public length: number;
1✔
40

1✔
41
  constructor() {
1✔
42
    this.stored = [];
63✔
43
  }
63✔
44

1✔
45
  static escapeContent(cache: string[], flag: string, str: string) {
1✔
46
    return `<!--${flag}\uFFFC${cache.push(str) - 1}-->`;
94✔
47
  }
94✔
48

1✔
49
  static restoreContent(cache: string[]) {
1✔
50
    return (_: string, index: number) => {
189✔
51
      assert(cache[index]);
93✔
52
      const value = cache[index];
93✔
53
      cache[index] = null;
93✔
54
      return value;
93✔
55
    };
189✔
56
  }
189✔
57

1✔
58
  restoreAllSwigTags(str: string) {
1✔
59
    const restored = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored));
63✔
60
    return restored;
63✔
61
  }
63✔
62

1✔
63
  restoreCodeBlocks(str: string) {
1✔
64
    return str.replace(rCodeBlockPlaceHolder, PostRenderEscape.restoreContent(this.stored));
63✔
65
  }
63✔
66

1✔
67
  restoreComments(str: string) {
1✔
68
    return str.replace(rCommentHolder, PostRenderEscape.restoreContent(this.stored));
63✔
69
  }
63✔
70

1✔
71
  escapeComments(str: string) {
1✔
UNCOV
72
    return str.replace(rCommentEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'comment', content));
×
UNCOV
73
  }
×
74

1✔
75
  escapeCodeBlocks(str: string) {
1✔
76
    return str.replace(rHexoPostRenderEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'code', content));
63✔
77
  }
63✔
78

1✔
79
  /**
1✔
80
   * @param {string} str
1✔
81
   * @returns string
1✔
82
   */
1✔
83
  escapeAllSwigTags(str: string) {
1✔
84
    let state = STATE_PLAINTEXT;
42✔
85
    let buffer_start = -1;
42✔
86
    let plaintext_comment_start = -1;
42✔
87
    let plain_text_start = 0;
42✔
88
    let output = '';
42✔
89

42✔
90
    let swig_tag_name_begin = false;
42✔
91
    let swig_tag_name_end = false;
42✔
92
    let swig_tag_name = '';
42✔
93

42✔
94
    let swig_full_tag_start_start = -1;
42✔
95
    let swig_full_tag_start_end = -1;
42✔
96
    // current we just consider one level of string quote
42✔
97
    let swig_string_quote = '';
42✔
98

42✔
99
    const { length } = str;
42✔
100

42✔
101
    let idx = 0;
42✔
102

42✔
103
    // for backtracking
42✔
104
    const swig_start_idx = [0, 0, 0, 0, 0];
42✔
105

42✔
106
    const flushPlainText = (end: number) => {
42✔
107
      if (plain_text_start !== -1 && end > plain_text_start) {
85✔
108
        output += str.slice(plain_text_start, end);
58✔
109
      }
58✔
110
      plain_text_start = -1;
85✔
111
    };
42✔
112

42✔
113
    const ensurePlainTextStart = (position: number) => {
42✔
114
      if (plain_text_start === -1) {
824✔
115
        plain_text_start = position;
56✔
116
      }
56✔
117
    };
42✔
118

42✔
119
    const pushAndReset = (value: string) => {
42✔
120
      output += value;
84✔
121
      plain_text_start = -1;
84✔
122
    };
42✔
123

42✔
124
    while (idx < length) {
42✔
125
      while (idx < length) {
50✔
126
        const char = str[idx];
2,456✔
127
        const next_char = str[idx + 1];
2,456✔
128

2,456✔
129
        if (state === STATE_PLAINTEXT) { // From plain text to swig
2,456✔
130
          ensurePlainTextStart(idx);
824✔
131
          if (char === '{') {
824✔
132
            // check if it is a complete tag {{ }}
75✔
133
            if (next_char === '{') {
75✔
134
              flushPlainText(idx);
21✔
135
              state = STATE_SWIG_VAR;
21✔
136
              idx++;
21✔
137
              buffer_start = idx + 1;
21✔
138
              swig_start_idx[state] = idx;
21✔
139
            } else if (next_char === '#') {
75✔
140
              flushPlainText(idx);
3✔
141
              state = STATE_SWIG_COMMENT;
3✔
142
              idx++;
3✔
143
              buffer_start = idx + 1;
3✔
144
              swig_start_idx[state] = idx;
3✔
145
            } else if (next_char === '%') {
54✔
146
              flushPlainText(idx);
50✔
147
              state = STATE_SWIG_TAG;
50✔
148
              idx++;
50✔
149
              buffer_start = idx + 1;
50✔
150
              swig_full_tag_start_start = idx + 1;
50✔
151
              swig_full_tag_start_end = idx + 1;
50✔
152
              swig_tag_name = '';
50✔
153
              swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag
50✔
154
              swig_tag_name_end = false;
50✔
155
              swig_start_idx[state] = idx;
50✔
156
            }
50✔
157
          }
75✔
158
          if (char === '<' && next_char === '!' && str[idx + 2] === '-' && str[idx + 3] === '-') {
824✔
159
            flushPlainText(idx);
11✔
160
            state = STATE_PLAINTEXT_COMMENT;
11✔
161
            plaintext_comment_start = idx;
11✔
162
            idx += 3;
11✔
163
          }
11✔
164
        } else if (state === STATE_SWIG_TAG) {
2,456✔
165
          if (char === '"' || char === '\'') {
772✔
166
            if (swig_string_quote === '') {
6✔
167
              swig_string_quote = char;
3✔
168
            } else if (swig_string_quote === char) {
3✔
169
              swig_string_quote = '';
3✔
170
            }
3✔
171
          }
6✔
172
          if (char === '%' && next_char === '}' && swig_string_quote === '') { // From swig back to plain text
772✔
173
            idx++;
46✔
174
            if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) {
46✔
175
              state = STATE_SWIG_FULL_TAG;
42✔
176
              buffer_start = idx + 1;
42✔
177
              // since we have already move idx to next char of '}', so here is idx -1
42✔
178
              swig_full_tag_start_end = idx - 1;
42✔
179
              swig_start_idx[state] = idx;
42✔
180
            } else {
46✔
181
              swig_tag_name = '';
4✔
182
              state = STATE_PLAINTEXT;
4✔
183
              // since we have already move idx to next char of '}', so here is idx -1
4✔
184
              pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${str.slice(buffer_start, idx - 1)}%}`));
4✔
185
            }
4✔
186

46✔
187
          } else {
772✔
188
            if (isNonWhiteSpaceChar(char)) {
726✔
189
              if (!swig_tag_name_begin && !swig_tag_name_end) {
584✔
190
                swig_tag_name_begin = true;
49✔
191
              }
49✔
192

584✔
193
              if (swig_tag_name_begin) {
584✔
194
                swig_tag_name += char;
346✔
195
              }
346✔
196
            } else {
726✔
197
              if (swig_tag_name_begin === true) {
142✔
198
                swig_tag_name_begin = false;
48✔
199
                swig_tag_name_end = true;
48✔
200
              }
48✔
201
            }
142✔
202
          }
726✔
203
        } else if (state === STATE_SWIG_VAR) {
1,632✔
204
          if (char === '"' || char === '\'') {
133✔
205
            if (swig_string_quote === '') {
6✔
206
              swig_string_quote = char;
3✔
207
            } else if (swig_string_quote === char) {
3✔
208
              swig_string_quote = '';
3✔
209
            }
3✔
210
          }
6✔
211
          // {{ }
133✔
212
          if (char === '}' && next_char !== '}' && swig_string_quote === '') {
133✔
213
            // From swig back to plain text
2✔
214
            state = STATE_PLAINTEXT;
2✔
215
            pushAndReset(`{{${str.slice(buffer_start, idx)}${char}`);
2✔
216
          } else if (char === '}' && next_char === '}' && swig_string_quote === '') {
133✔
217
            pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{{${str.slice(buffer_start, idx)}}}`));
18✔
218
            idx++;
18✔
219
            state = STATE_PLAINTEXT;
18✔
220
          }
18✔
221
        } else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text
860✔
222
          if (char === '#' && next_char === '}') {
31✔
223
            idx++;
1✔
224
            state = STATE_PLAINTEXT;
1✔
225
            plain_text_start = -1;
1✔
226
          }
1✔
227
        } else if (state === STATE_SWIG_FULL_TAG) {
727✔
228
          if (char === '{' && next_char === '%') {
571✔
229
            let swig_full_tag_end_buffer = '';
44✔
230
            let swig_full_tag_found = false;
44✔
231

44✔
232
            let _idx = idx + 2;
44✔
233
            for (; _idx < length; _idx++) {
44✔
234
              const _char = str[_idx];
590✔
235
              const _next_char = str[_idx + 1];
590✔
236

590✔
237
              if (_char === '%' && _next_char === '}') {
590✔
238
                _idx++;
43✔
239
                swig_full_tag_found = true;
43✔
240
                break;
43✔
241
              }
43✔
242

547✔
243
              swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char;
547✔
244
            }
547✔
245

44✔
246
            if (swig_full_tag_found && swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) {
44✔
247
              state = STATE_PLAINTEXT;
41✔
248
              pushAndReset(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%}${str.slice(buffer_start, idx)}{%${swig_full_tag_end_buffer}%}`));
41✔
249
              idx = _idx;
41✔
250
              swig_full_tag_end_buffer = '';
41✔
251
            }
41✔
252
          }
44✔
253
        } else if (state === STATE_PLAINTEXT_COMMENT) {
696✔
254
          if (char === '-' && next_char === '-' && str[idx + 2] === '>') {
125✔
255
            state = STATE_PLAINTEXT;
10✔
256
            const comment = str.slice(plaintext_comment_start, idx + 3);
10✔
257
            pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment));
10✔
258
            idx += 2;
10✔
259
          }
10✔
260
        }
125✔
261
        idx++;
2,456✔
262
      }
2,456✔
263
      if (state === STATE_PLAINTEXT) {
50✔
264
        break;
41✔
265
      }
41✔
266
      if (state === STATE_PLAINTEXT_COMMENT) {
50✔
267
        // Unterminated comment, just push the rest as comment
1✔
268
        const comment = str.slice(plaintext_comment_start, length);
1✔
269
        pushAndReset(PostRenderEscape.escapeContent(this.stored, 'comment', comment));
1✔
270
        break;
1✔
271
      }
1✔
272
      // If the swig tag is not closed, then it is a plain text, we need to backtrack
8✔
273
      if (state === STATE_SWIG_FULL_TAG) {
50✔
274
        pushAndReset(`{%${str.slice(swig_full_tag_start_start, swig_full_tag_start_end)}%`);
1✔
275
      } else {
50✔
276
        pushAndReset('{');
7✔
277
      }
7✔
278
      idx = swig_start_idx[state];
8✔
279
      swig_string_quote = '';
8✔
280
      state = STATE_PLAINTEXT;
8✔
281
    }
8✔
282

42✔
283
    if (plain_text_start !== -1 && plain_text_start < length) {
42✔
284
      output += str.slice(plain_text_start);
13✔
285
    }
13✔
286

42✔
287
    return output;
42✔
288
  }
42✔
289
}
1✔
290

1✔
291
const prepareFrontMatter = (data: any, jsonMode: boolean): Record<string, string> => {
1✔
292
  for (const [key, item] of Object.entries(data)) {
69✔
293
    if (moment.isMoment(item)) {
344✔
294
      data[key] = item.utc().format('YYYY-MM-DD HH:mm:ss');
69✔
295
    } else if (moment.isDate(item)) {
344!
296
      data[key] = moment.utc(item).format('YYYY-MM-DD HH:mm:ss');
×
297
    } else if (typeof item === 'string') {
275✔
298
      if (jsonMode || item.includes(':') || item.startsWith('#') || item.startsWith('!!')
230✔
299
      || item.includes('{') || item.includes('}') || item.includes('[') || item.includes(']')
230✔
300
      || item.includes('\'') || item.includes('"')) data[key] = `"${item.replace(/"/g, '\\"')}"`;
230✔
301
    }
230✔
302
  }
344✔
303

69✔
304
  return data;
69✔
305
};
69✔
306

1✔
307

1✔
308
const removeExtname = (str: string) => {
1✔
309
  return str.substring(0, str.length - extname(str).length);
6✔
310
};
6✔
311

1✔
312
const createAssetFolder = (path: string, assetFolder: boolean) => {
1✔
313
  if (!assetFolder) return Promise.resolve();
69✔
314

4✔
315
  const target = removeExtname(path);
4✔
316

4✔
317
  if (basename(target) === 'index') return Promise.resolve();
69✔
318

3✔
319
  return exists(target).then(exist => {
3✔
320
    if (!exist) return mkdirs(target);
3✔
321
  });
3✔
322
};
3✔
323

1✔
324
interface Result {
1✔
325
  path: string;
1✔
326
  content: string;
1✔
327
}
1✔
328

1✔
329
interface PostData {
1✔
330
  title?: string | number;
1✔
331
  layout?: string;
1✔
332
  slug?: string | number;
1✔
333
  path?: string;
1✔
334
  date?: moment.Moment;
1✔
335
  [prop: string]: any;
1✔
336
}
1✔
337

1✔
338
class Post {
1✔
339
  public context: Hexo;
1✔
340

1✔
341
  constructor(context: Hexo) {
1✔
342
    this.context = context;
147✔
343
  }
147✔
344

1✔
345
  create(data: PostData, callback?: NodeJSLikeCallback<any>): Promise<Result>;
1✔
346
  create(data: PostData, replace: boolean, callback?: NodeJSLikeCallback<any>): Promise<Result>;
1✔
347
  create(data: PostData, replace: boolean | (NodeJSLikeCallback<any>), callback?: NodeJSLikeCallback<any>): Promise<Result> {
1✔
348
    if (!callback && typeof replace === 'function') {
69✔
349
      callback = replace;
1✔
350
      replace = false;
1✔
351
    }
1✔
352

69✔
353
    const ctx = this.context;
69✔
354
    const { config } = ctx;
69✔
355

69✔
356
    data.slug = slugize((data.slug || data.title).toString(), { transform: config.filename_case });
69✔
357
    data.layout = (data.layout || config.default_layout).toLowerCase();
69✔
358
    data.date = data.date ? moment(data.date) : moment();
69!
359

69✔
360
    return Promise.all([
69✔
361
      // Get the post path
69✔
362
      ctx.execFilter('new_post_path', data, {
69✔
363
        args: [replace],
69✔
364
        context: ctx
69✔
365
      }),
69✔
366
      this._renderScaffold(data)
69✔
367
    ]).spread((path: string, content: string) => {
69✔
368
      const result = { path, content };
69✔
369

69✔
370
      return Promise.all<void, void | string>([
69✔
371
        // Write content to file
69✔
372
        writeFile(path, content),
69✔
373
        // Create asset folder
69✔
374
        createAssetFolder(path, config.post_asset_folder)
69✔
375
      ]).then(() => {
69✔
376
        ctx.emit('new', result);
69✔
377
        return result;
69✔
378
      });
69✔
379
    }).asCallback(callback);
69✔
380
  }
69✔
381

1✔
382
  _getScaffold(layout: string) {
1✔
383
    const ctx = this.context;
69✔
384

69✔
385
    return ctx.scaffold.get(layout).then(result => {
69✔
386
      if (result != null) return result;
69✔
387
      return ctx.scaffold.get('normal');
4✔
388
    });
69✔
389
  }
69✔
390

1✔
391
  _renderScaffold(data: PostData) {
1✔
392
    const { tag } = this.context.extend;
69✔
393
    let splitted: ReturnType<typeof yfmSplit>;
69✔
394

69✔
395
    return this._getScaffold(data.layout).then(scaffold => {
69✔
396
      splitted = yfmSplit(scaffold);
69✔
397
      const jsonMode = splitted.separator.startsWith(';');
69✔
398
      const frontMatter = prepareFrontMatter({ ...data }, jsonMode);
69✔
399

69✔
400
      return tag.render(splitted.data, frontMatter);
69✔
401
    }).then(frontMatter => {
69✔
402
      const { separator } = splitted;
69✔
403
      const jsonMode = separator.startsWith(';');
69✔
404

69✔
405
      // Parse front-matter
69✔
406
      let obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : load(frontMatter);
69✔
407

69✔
408
      obj = deepMerge(obj, Object.fromEntries(Object.entries(data).filter(([key, value]) => !preservedKeys.includes(key) && value != null)));
69✔
409

69✔
410
      let content = '';
69✔
411
      // Prepend the separator
69✔
412
      if (splitted.prefixSeparator) content += `${separator}\n`;
69✔
413

69✔
414
      content += yfmStringify(obj, {
69✔
415
        mode: jsonMode ? 'json' : ''
69✔
416
      });
69✔
417

69✔
418
      // Concat content
69✔
419
      content += splitted.content;
69✔
420

69✔
421
      if (data.content) {
69✔
422
        content += `\n${data.content}`;
1✔
423
      }
1✔
424

69✔
425
      return content;
69✔
426
    });
69✔
427
  }
69✔
428

1✔
429
  publish(data: PostData, replace?: boolean): Promise<Result>;
1✔
430
  publish(data: PostData, callback?: NodeJSLikeCallback<Result>): Promise<Result>;
1✔
431
  publish(data: PostData, replace: boolean, callback?: NodeJSLikeCallback<Result>): Promise<Result>;
1✔
432
  publish(data: PostData, replace?: boolean | NodeJSLikeCallback<Result>, callback?: NodeJSLikeCallback<Result>): Promise<Result> {
1✔
433
    if (!callback && typeof replace === 'function') {
13✔
434
      callback = replace;
1✔
435
      replace = false;
1✔
436
    }
1✔
437

13✔
438
    if (data.layout === 'draft') data.layout = 'post';
13!
439

13✔
440
    const ctx = this.context;
13✔
441
    const { config } = ctx;
13✔
442
    const draftDir = join(ctx.source_dir, '_drafts');
13✔
443
    const slug = slugize(data.slug.toString(), { transform: config.filename_case });
13✔
444
    data.slug = slug;
13✔
445
    const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\/\\\\]+)`);
13✔
446
    let src = '';
13✔
447
    const result: Result = {} as any;
13✔
448

13✔
449
    data.layout = (data.layout || config.default_layout).toLowerCase();
13✔
450

13✔
451
    // Find the draft
13✔
452
    return listDir(draftDir).then(list => {
13✔
453
      const item = list.find(item => regex.test(item));
13✔
454
      if (!item) throw new Error(`Draft "${slug}" does not exist.`);
13!
455

13✔
456
      // Read the content
13✔
457
      src = join(draftDir, item);
13✔
458
      return readFile(src);
13✔
459
    }).then(content => {
13✔
460
      // Create post
13✔
461
      Object.assign(data, yfmParse(content));
13✔
462
      data.content = data._content;
13✔
463
      data._content = undefined;
13✔
464

13✔
465
      return this.create(data, replace as boolean);
13✔
466
    }).then(post => {
13✔
467
      result.path = post.path;
13✔
468
      result.content = post.content;
13✔
469
      return unlink(src);
13✔
470
    }).then(() => { // Remove the original draft file
13✔
471
      if (!config.post_asset_folder) return;
13✔
472

1✔
473
      // Copy assets
1✔
474
      const assetSrc = removeExtname(src);
1✔
475
      const assetDest = removeExtname(result.path);
1✔
476

1✔
477
      return exists(assetSrc).then(exist => {
1✔
478
        if (!exist) return;
1!
479

1✔
480
        return copyDir(assetSrc, assetDest).then(() => rmdir(assetSrc));
1✔
481
      });
1✔
482
    }).thenReturn(result).asCallback(callback);
13✔
483
  }
13✔
484

1✔
485
  render(source: string, data: RenderData = {}, callback?: NodeJSLikeCallback<never>) {
1✔
486
    const ctx = this.context;
67✔
487
    const { config } = ctx;
67✔
488
    const { tag } = ctx.extend;
67✔
489
    const ext = data.engine || (source ? extname(source) : '');
67✔
490

67✔
491
    let promise;
67✔
492

67✔
493
    if (data.content != null) {
67✔
494
      promise = Promise.resolve(data.content);
65✔
495
    } else if (source) {
67✔
496
      // Read content from files
1✔
497
      promise = readFile(source);
1✔
498
    } else {
1✔
499
      return Promise.reject(new Error('No input file or string!')).asCallback(callback);
1✔
500
    }
1✔
501

66✔
502
    // Files like js and css are also processed by this function, but they do not require preprocessing like markdown
66✔
503
    // data.source does not exist when tag plugins call the markdown renderer
66✔
504
    const isPost = !data.source || ['html', 'htm'].includes(ctx.render.getOutput(data.source));
67✔
505

67✔
506
    if (!isPost) {
67✔
507
      return promise.then(content => {
3✔
508
        data.content = content;
3✔
509
        ctx.log.debug('Rendering file: %s', magenta(source));
3✔
510

3✔
511
        return ctx.render.render({
3✔
512
          text: data.content,
3✔
513
          path: source,
3✔
514
          engine: data.engine,
3✔
515
          toString: true
3✔
516
        });
3✔
517
      }).then(content => {
3✔
518
        data.content = content;
3✔
519
        return data;
3✔
520
      }).asCallback(callback);
3✔
521
    }
3✔
522

63✔
523
    // disable Nunjucks when the renderer specify that.
63✔
524
    let disableNunjucks = ext && ctx.render.renderer.get(ext) && !!ctx.render.renderer.get(ext).disableNunjucks;
67✔
525

67✔
526
    // front-matter overrides renderer's option
67✔
527
    if (typeof data.disableNunjucks === 'boolean') disableNunjucks = data.disableNunjucks;
67✔
528

63✔
529
    const cacheObj = new PostRenderEscape();
63✔
530

63✔
531
    return promise.then(content => {
63✔
532
      data.content = content;
63✔
533
      // Run "before_post_render" filters
63✔
534
      return ctx.execFilter('before_post_render', data, { context: ctx });
63✔
535
    }).then(() => {
63✔
536
      // Escape all comments to avoid conflict with Nunjucks and code block
63✔
537
      data.content = cacheObj.escapeCodeBlocks(data.content);
63✔
538
      // Escape all Nunjucks/Swig tags
63✔
539
      let hasSwigTag = true;
63✔
540
      if (disableNunjucks === false) {
63✔
541
        hasSwigTag = rSwigTag.test(data.content);
57✔
542
        if (hasSwigTag) {
57✔
543
          data.content = cacheObj.escapeAllSwigTags(data.content);
42✔
544
        }
42✔
545
      }
57✔
546

63✔
547
      const options: { highlight?: boolean; } = data.markdown || {};
63✔
548
      if (!config.syntax_highlighter) options.highlight = null;
63!
549

63✔
550
      ctx.log.debug('Rendering post: %s', magenta(source));
63✔
551
      // Render with markdown or other renderer
63✔
552
      return ctx.render.render({
63✔
553
        text: data.content,
63✔
554
        path: source,
63✔
555
        engine: data.engine,
63✔
556
        toString: true,
63✔
557
        onRenderEnd(content) {
63✔
558
          // Replace cache data with real contents
63✔
559
          data.content = cacheObj.restoreAllSwigTags(content);
63✔
560

63✔
561
          // Return content after replace the placeholders
63✔
562
          if (disableNunjucks || !hasSwigTag) return data.content;
63✔
563

44✔
564
          // Render with Nunjucks if there are Swig tags
44✔
565
          return tag.render(data.content, data);
44✔
566
        }
44✔
567
      }, options);
63✔
568
    }).then(content => {
63✔
569
      data.content = cacheObj.restoreComments(content);
63✔
570
      data.content = cacheObj.restoreCodeBlocks(data.content);
63✔
571

63✔
572
      // Run "after_post_render" filters
63✔
573
      return ctx.execFilter('after_post_render', data, { context: ctx });
63✔
574
    }).asCallback(callback);
63✔
575
  }
63✔
576
}
1✔
577

1✔
578
export = Post;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc