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

hexojs / hexo / 14007045798

22 Mar 2025 08:52AM UTC coverage: 99.517% (-0.009%) from 99.526%
14007045798

Pull #5636

github

web-flow
Merge de619bbf7 into 25dd2acf0
Pull Request #5636: fix(escapeAllSwigTags): Prevent swig tag prefix collision during esca…

2397 of 2493 branches covered (96.15%)

17 of 17 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

9887 of 9935 relevant lines covered (99.52%)

55.37 hits per line

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

99.64
/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

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

1✔
20
const STATE_PLAINTEXT = Symbol('plaintext');
1✔
21
const STATE_SWIG_VAR = Symbol('swig_var');
1✔
22
const STATE_SWIG_COMMENT = Symbol('swig_comment');
1✔
23
const STATE_SWIG_TAG = Symbol('swig_tag');
1✔
24
const STATE_SWIG_FULL_TAG = Symbol('swig_full_tag');
1✔
25

1✔
26
const isNonWhiteSpaceChar = (char: string) => char !== '\r'
1✔
27
  && char !== '\n'
643✔
28
  && char !== '\t'
643✔
29
  && char !== '\f'
643✔
30
  && char !== '\v'
643✔
31
  && char !== ' ';
1✔
32

1✔
33
class StringBuilder {
1✔
34
  private parts: string[];
1✔
35

1✔
36
  constructor() {
1✔
37
    this.parts = [];
35✔
38
  }
35✔
39

1✔
40
  append(str: string): void {
1✔
41
    this.parts.push(str);
896✔
42
  }
896✔
43

1✔
44
  toString(): string {
1✔
45
    return this.parts.join('');
35✔
46
  }
35✔
47
}
1✔
48

1✔
49
class PostRenderEscape {
1✔
50
  public stored: string[];
1✔
51
  public length: number;
1✔
52

1✔
53
  constructor() {
1✔
54
    this.stored = [];
54✔
55
  }
54✔
56

1✔
57
  static escapeContent(cache: string[], flag: string, str: string) {
1✔
58
    return `<!--${flag}\uFFFC${cache.push(str) - 1}-->`;
79✔
59
  }
79✔
60

1✔
61
  static restoreContent(cache: string[]) {
1✔
62
    return (_: string, index: number) => {
107✔
63
      assert(cache[index]);
78✔
64
      const value = cache[index];
78✔
65
      cache[index] = null;
78✔
66
      return value;
78✔
67
    };
107✔
68
  }
107✔
69

1✔
70
  restoreAllSwigTags(str: string) {
1✔
71
    const restored = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored));
54✔
72
    return restored;
54✔
73
  }
54✔
74

1✔
75
  restoreCodeBlocks(str: string) {
1✔
76
    return str.replace(rCodeBlockPlaceHolder, PostRenderEscape.restoreContent(this.stored));
53✔
77
  }
53✔
78

1✔
79
  escapeCodeBlocks(str: string) {
1✔
80
    return str.replace(rHexoPostRenderEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'code', content));
54✔
81
  }
54✔
82

1✔
83
  /**
1✔
84
   * @param {string} str
1✔
85
   * @returns string
1✔
86
   */
1✔
87
  escapeAllSwigTags(str: string, block_swig_tag_map: { [name: string]: boolean }) {
1✔
88
    if (!/(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/s.test(str)) {
48✔
89
      return str;
13✔
90
    }
13✔
91
    let state = STATE_PLAINTEXT;
35✔
92
    let buffer = '';
35✔
93
    const output = new StringBuilder();
35✔
94

35✔
95
    let swig_tag_name_begin = false;
35✔
96
    let swig_tag_name_end = false;
35✔
97
    let swig_tag_name = '';
35✔
98
    let swig_full_tag_start_buffer = '';
35✔
99
    // current we just consider one level of string quote
35✔
100
    let swig_string_quote = '';
35✔
101

35✔
102
    const { length } = str;
35✔
103

35✔
104
    let idx = 0;
35✔
105

35✔
106
    // for backtracking
35✔
107
    const swig_start_idx = {
35✔
108
      [STATE_SWIG_VAR]: 0,
35✔
109
      [STATE_SWIG_COMMENT]: 0,
35✔
110
      [STATE_SWIG_TAG]: 0,
35✔
111
      [STATE_SWIG_FULL_TAG]: 0
35✔
112
    };
35✔
113

35✔
114
    while (idx < length) {
48✔
115
      while (idx < length) {
39✔
116
        const char = str[idx];
2,237✔
117
        const next_char = str[idx + 1];
2,237✔
118

2,237✔
119
        if (state === STATE_PLAINTEXT) { // From plain text to swig
2,237✔
120
          if (char === '{') {
897✔
121
            // check if it is a complete tag {{ }}
71✔
122
            if (next_char === '{') {
71✔
123
              state = STATE_SWIG_VAR;
21✔
124
              idx++;
21✔
125
              swig_start_idx[state] = idx;
21✔
126
            } else if (next_char === '#') {
71✔
127
              state = STATE_SWIG_COMMENT;
3✔
128
              idx++;
3✔
129
              swig_start_idx[state] = idx;
3✔
130
            } else if (next_char === '%') {
50✔
131
              state = STATE_SWIG_TAG;
46✔
132
              idx++;
46✔
133
              swig_tag_name = '';
46✔
134
              swig_full_tag_start_buffer = '';
46✔
135
              swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag
46✔
136
              swig_tag_name_end = false;
46✔
137
              swig_start_idx[state] = idx;
46✔
138
            } else {
47✔
139
              output.append(char);
1✔
140
            }
1✔
141
          } else {
897✔
142
            output.append(char);
826✔
143
          }
826✔
144
        } else if (state === STATE_SWIG_TAG) {
2,237✔
145
          if (char === '"' || char === '\'') {
688✔
146
            if (swig_string_quote === '') {
6✔
147
              swig_string_quote = char;
3✔
148
            } else if (swig_string_quote === char) {
3✔
149
              swig_string_quote = '';
3✔
150
            }
3✔
151
          }
6✔
152
          // {% } or {% %
688✔
153
          if (((char !== '%' && next_char === '}') || (char === '%' && next_char !== '}')) && swig_string_quote === '') {
688✔
154
            // From swig back to plain text
3✔
155
            swig_tag_name = '';
3✔
156
            state = STATE_PLAINTEXT;
3✔
157
            output.append(`{%${buffer}${char}`);
3✔
158
            buffer = '';
3✔
159
          } else if (char === '%' && next_char === '}' && swig_string_quote === '') { // From swig back to plain text
688✔
160
            idx++;
42✔
161
            if (swig_tag_name !== '' && (block_swig_tag_map[swig_tag_name] ?? false)) {
42✔
162
              state = STATE_SWIG_FULL_TAG;
36✔
163
              swig_start_idx[state] = idx;
36✔
164
            } else {
42✔
165
              swig_tag_name = '';
6✔
166
              state = STATE_PLAINTEXT;
6✔
167
              output.append(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${buffer}%}`));
6✔
168
            }
6✔
169

42✔
170
            buffer = '';
42✔
171
          } else {
685✔
172
            buffer = buffer + char;
643✔
173
            swig_full_tag_start_buffer = swig_full_tag_start_buffer + char;
643✔
174

643✔
175
            if (isNonWhiteSpaceChar(char)) {
643✔
176
              if (!swig_tag_name_begin && !swig_tag_name_end) {
521✔
177
                swig_tag_name_begin = true;
45✔
178
              }
45✔
179

521✔
180
              if (swig_tag_name_begin) {
521✔
181
                swig_tag_name += char;
323✔
182
              }
323✔
183
            } else {
643✔
184
              if (swig_tag_name_begin === true) {
122✔
185
                swig_tag_name_begin = false;
42✔
186
                swig_tag_name_end = true;
42✔
187
              }
42✔
188
            }
122✔
189
          }
643✔
190
        } else if (state === STATE_SWIG_VAR) {
1,340✔
191
          if (char === '"' || char === '\'') {
133✔
192
            if (swig_string_quote === '') {
6✔
193
              swig_string_quote = char;
3✔
194
            } else if (swig_string_quote === char) {
3✔
195
              swig_string_quote = '';
3✔
196
            }
3✔
197
          }
6✔
198
          // {{ }
133✔
199
          if (char === '}' && next_char !== '}' && swig_string_quote === '') {
133✔
200
            // From swig back to plain text
2✔
201
            state = STATE_PLAINTEXT;
2✔
202
            output.append(`{{${buffer}${char}`);
2✔
203
            buffer = '';
2✔
204
          } else if (char === '}' && next_char === '}' && swig_string_quote === '') {
133✔
205
            idx++;
18✔
206
            state = STATE_PLAINTEXT;
18✔
207
            output.append(PostRenderEscape.escapeContent(this.stored, 'swig', `{{${buffer}}}`));
18✔
208
            buffer = '';
18✔
209
          } else {
131✔
210
            buffer = buffer + char;
113✔
211
          }
113✔
212
        } else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text
652✔
213
          if (char === '#' && next_char === '}') {
31✔
214
            idx++;
1✔
215
            state = STATE_PLAINTEXT;
1✔
216
            buffer = '';
1✔
217
          }
1✔
218
        } else if (state === STATE_SWIG_FULL_TAG) {
519✔
219
          if (char === '{' && next_char === '%') {
488✔
220
            let swig_full_tag_end_buffer = '';
38✔
221
            let swig_full_tag_found = false;
38✔
222

38✔
223
            let _idx = idx + 2;
38✔
224
            for (; _idx < length; _idx++) {
38✔
225
              const _char = str[_idx];
517✔
226
              const _next_char = str[_idx + 1];
517✔
227

517✔
228
              if (_char === '%' && _next_char === '}') {
517✔
229
                _idx++;
38✔
230
                swig_full_tag_found = true;
38✔
231
                break;
38✔
232
              }
38✔
233

479✔
234
              swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char;
479✔
235
            }
479✔
236

38✔
237
            if (swig_full_tag_found && swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) {
38✔
238
              state = STATE_PLAINTEXT;
36✔
239
              output.append(PostRenderEscape.escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`));
36✔
240
              idx = _idx;
36✔
241
              swig_full_tag_start_buffer = '';
36✔
242
              swig_full_tag_end_buffer = '';
36✔
243
              buffer = '';
36✔
244
            } else {
38✔
245
              buffer += char;
2✔
246
            }
2✔
247
          } else {
488✔
248
            buffer += char;
450✔
249
          }
450✔
250
        }
488✔
251
        idx++;
2,237✔
252
      }
2,237✔
253
      if (state === STATE_PLAINTEXT) {
39✔
254
        break;
35✔
255
      }
35✔
256
      // If the swig tag is not closed, then it is a plain text, we need to backtrack
4✔
257
      idx = swig_start_idx[state];
4✔
258
      buffer = '';
4✔
259
      swig_string_quote = '';
4✔
260
      if (state === STATE_SWIG_FULL_TAG) {
39!
UNCOV
261
        output.append(`{%${swig_full_tag_start_buffer}%`);
×
262
      } else {
39✔
263
        output.append('{');
4✔
264
      }
4✔
265
      swig_full_tag_start_buffer = '';
4✔
266
      state = STATE_PLAINTEXT;
4✔
267
    }
4✔
268

35✔
269
    return output.toString();
35✔
270
  }
35✔
271
}
1✔
272

1✔
273
const prepareFrontMatter = (data: any, jsonMode: boolean): Record<string, string> => {
1✔
274
  for (const [key, item] of Object.entries(data)) {
69✔
275
    if (moment.isMoment(item)) {
344✔
276
      data[key] = item.utc().format('YYYY-MM-DD HH:mm:ss');
69✔
277
    } else if (moment.isDate(item)) {
344!
278
      data[key] = moment.utc(item).format('YYYY-MM-DD HH:mm:ss');
×
279
    } else if (typeof item === 'string') {
275✔
280
      if (jsonMode || item.includes(':') || item.startsWith('#') || item.startsWith('!!')
230✔
281
      || item.includes('{') || item.includes('}') || item.includes('[') || item.includes(']')
230✔
282
      || item.includes('\'') || item.includes('"')) data[key] = `"${item.replace(/"/g, '\\"')}"`;
230✔
283
    }
230✔
284
  }
344✔
285

69✔
286
  return data;
69✔
287
};
69✔
288

1✔
289

1✔
290
const removeExtname = (str: string) => {
1✔
291
  return str.substring(0, str.length - extname(str).length);
6✔
292
};
6✔
293

1✔
294
const createAssetFolder = (path: string, assetFolder: boolean) => {
1✔
295
  if (!assetFolder) return Promise.resolve();
69✔
296

4✔
297
  const target = removeExtname(path);
4✔
298

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

3✔
301
  return exists(target).then(exist => {
3✔
302
    if (!exist) return mkdirs(target);
3✔
303
  });
3✔
304
};
3✔
305

1✔
306
interface Result {
1✔
307
  path: string;
1✔
308
  content: string;
1✔
309
}
1✔
310

1✔
311
interface PostData {
1✔
312
  title?: string | number;
1✔
313
  layout?: string;
1✔
314
  slug?: string | number;
1✔
315
  path?: string;
1✔
316
  date?: moment.Moment;
1✔
317
  [prop: string]: any;
1✔
318
}
1✔
319

1✔
320
class Post {
1✔
321
  public context: Hexo;
1✔
322

1✔
323
  constructor(context: Hexo) {
1✔
324
    this.context = context;
147✔
325
  }
147✔
326

1✔
327
  create(data: PostData, callback?: NodeJSLikeCallback<any>): Promise<Result>;
1✔
328
  create(data: PostData, replace: boolean, callback?: NodeJSLikeCallback<any>): Promise<Result>;
1✔
329
  create(data: PostData, replace: boolean | (NodeJSLikeCallback<any>), callback?: NodeJSLikeCallback<any>): Promise<Result> {
1✔
330
    if (!callback && typeof replace === 'function') {
69✔
331
      callback = replace;
1✔
332
      replace = false;
1✔
333
    }
1✔
334

69✔
335
    const ctx = this.context;
69✔
336
    const { config } = ctx;
69✔
337

69✔
338
    data.slug = slugize((data.slug || data.title).toString(), { transform: config.filename_case });
69✔
339
    data.layout = (data.layout || config.default_layout).toLowerCase();
69✔
340
    data.date = data.date ? moment(data.date) : moment();
69!
341

69✔
342
    return Promise.all([
69✔
343
      // Get the post path
69✔
344
      ctx.execFilter('new_post_path', data, {
69✔
345
        args: [replace],
69✔
346
        context: ctx
69✔
347
      }),
69✔
348
      this._renderScaffold(data)
69✔
349
    ]).spread((path: string, content: string) => {
69✔
350
      const result = { path, content };
69✔
351

69✔
352
      return Promise.all<void, void | string>([
69✔
353
        // Write content to file
69✔
354
        writeFile(path, content),
69✔
355
        // Create asset folder
69✔
356
        createAssetFolder(path, config.post_asset_folder)
69✔
357
      ]).then(() => {
69✔
358
        ctx.emit('new', result);
69✔
359
        return result;
69✔
360
      });
69✔
361
    }).asCallback(callback);
69✔
362
  }
69✔
363

1✔
364
  _getScaffold(layout: string) {
1✔
365
    const ctx = this.context;
69✔
366

69✔
367
    return ctx.scaffold.get(layout).then(result => {
69✔
368
      if (result != null) return result;
69✔
369
      return ctx.scaffold.get('normal');
4✔
370
    });
69✔
371
  }
69✔
372

1✔
373
  _renderScaffold(data: PostData) {
1✔
374
    const { tag } = this.context.extend;
69✔
375
    let splitted: ReturnType<typeof yfmSplit>;
69✔
376

69✔
377
    return this._getScaffold(data.layout).then(scaffold => {
69✔
378
      splitted = yfmSplit(scaffold);
69✔
379
      const jsonMode = splitted.separator.startsWith(';');
69✔
380
      const frontMatter = prepareFrontMatter({ ...data }, jsonMode);
69✔
381

69✔
382
      return tag.render(splitted.data, frontMatter);
69✔
383
    }).then(frontMatter => {
69✔
384
      const { separator } = splitted;
69✔
385
      const jsonMode = separator.startsWith(';');
69✔
386

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

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

69✔
392
      let content = '';
69✔
393
      // Prepend the separator
69✔
394
      if (splitted.prefixSeparator) content += `${separator}\n`;
69✔
395

69✔
396
      content += yfmStringify(obj, {
69✔
397
        mode: jsonMode ? 'json' : ''
69✔
398
      });
69✔
399

69✔
400
      // Concat content
69✔
401
      content += splitted.content;
69✔
402

69✔
403
      if (data.content) {
69✔
404
        content += `\n${data.content}`;
1✔
405
      }
1✔
406

69✔
407
      return content;
69✔
408
    });
69✔
409
  }
69✔
410

1✔
411
  publish(data: PostData, replace?: boolean): Promise<Result>;
1✔
412
  publish(data: PostData, callback?: NodeJSLikeCallback<Result>): Promise<Result>;
1✔
413
  publish(data: PostData, replace: boolean, callback?: NodeJSLikeCallback<Result>): Promise<Result>;
1✔
414
  publish(data: PostData, replace?: boolean | NodeJSLikeCallback<Result>, callback?: NodeJSLikeCallback<Result>): Promise<Result> {
1✔
415
    if (!callback && typeof replace === 'function') {
13✔
416
      callback = replace;
1✔
417
      replace = false;
1✔
418
    }
1✔
419

13✔
420
    if (data.layout === 'draft') data.layout = 'post';
13!
421

13✔
422
    const ctx = this.context;
13✔
423
    const { config } = ctx;
13✔
424
    const draftDir = join(ctx.source_dir, '_drafts');
13✔
425
    const slug = slugize(data.slug.toString(), { transform: config.filename_case });
13✔
426
    data.slug = slug;
13✔
427
    const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\/\\\\]+)`);
13✔
428
    let src = '';
13✔
429
    const result: Result = {} as any;
13✔
430

13✔
431
    data.layout = (data.layout || config.default_layout).toLowerCase();
13✔
432

13✔
433
    // Find the draft
13✔
434
    return listDir(draftDir).then(list => {
13✔
435
      const item = list.find(item => regex.test(item));
13✔
436
      if (!item) throw new Error(`Draft "${slug}" does not exist.`);
13!
437

13✔
438
      // Read the content
13✔
439
      src = join(draftDir, item);
13✔
440
      return readFile(src);
13✔
441
    }).then(content => {
13✔
442
      // Create post
13✔
443
      Object.assign(data, yfmParse(content));
13✔
444
      data.content = data._content;
13✔
445
      data._content = undefined;
13✔
446

13✔
447
      return this.create(data, replace as boolean);
13✔
448
    }).then(post => {
13✔
449
      result.path = post.path;
13✔
450
      result.content = post.content;
13✔
451
      return unlink(src);
13✔
452
    }).then(() => { // Remove the original draft file
13✔
453
      if (!config.post_asset_folder) return;
13✔
454

1✔
455
      // Copy assets
1✔
456
      const assetSrc = removeExtname(src);
1✔
457
      const assetDest = removeExtname(result.path);
1✔
458

1✔
459
      return exists(assetSrc).then(exist => {
1✔
460
        if (!exist) return;
1!
461

1✔
462
        return copyDir(assetSrc, assetDest).then(() => rmdir(assetSrc));
1✔
463
      });
1✔
464
    }).thenReturn(result).asCallback(callback);
13✔
465
  }
13✔
466

1✔
467
  render(source: string, data: RenderData = {}, callback?: NodeJSLikeCallback<never>) {
1✔
468
    const ctx = this.context;
58✔
469
    const { config } = ctx;
58✔
470
    const { tag } = ctx.extend;
58✔
471
    const ext = data.engine || (source ? extname(source) : '');
58✔
472

58✔
473
    let promise;
58✔
474

58✔
475
    if (data.content != null) {
58✔
476
      promise = Promise.resolve(data.content);
56✔
477
    } else if (source) {
58✔
478
      // Read content from files
1✔
479
      promise = readFile(source);
1✔
480
    } else {
1✔
481
      return Promise.reject(new Error('No input file or string!')).asCallback(callback);
1✔
482
    }
1✔
483

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

58✔
488
    if (!isPost) {
58✔
489
      return promise.then(content => {
3✔
490
        data.content = content;
3✔
491
        ctx.log.debug('Rendering file: %s', magenta(source));
3✔
492

3✔
493
        return ctx.render.render({
3✔
494
          text: data.content,
3✔
495
          path: source,
3✔
496
          engine: data.engine,
3✔
497
          toString: true
3✔
498
        });
3✔
499
      }).then(content => {
3✔
500
        data.content = content;
3✔
501
        return data;
3✔
502
      }).asCallback(callback);
3✔
503
    }
3✔
504

54✔
505
    // disable Nunjucks when the renderer specify that.
54✔
506
    let disableNunjucks = ext && ctx.render.renderer.get(ext) && !!ctx.render.renderer.get(ext).disableNunjucks;
58✔
507

58✔
508
    // front-matter overrides renderer's option
58✔
509
    if (typeof data.disableNunjucks === 'boolean') disableNunjucks = data.disableNunjucks;
58✔
510

54✔
511
    const cacheObj = new PostRenderEscape();
54✔
512

54✔
513
    return promise.then(content => {
54✔
514
      data.content = content;
54✔
515
      // Run "before_post_render" filters
54✔
516
      return ctx.execFilter('before_post_render', data, { context: ctx });
54✔
517
    }).then(() => {
54✔
518
      data.content = cacheObj.escapeCodeBlocks(data.content);
54✔
519
      // Escape all Nunjucks/Swig tags
54✔
520
      if (disableNunjucks === false) {
54✔
521
        data.content = cacheObj.escapeAllSwigTags(data.content, tag.block_swig_tag_map);
48✔
522
      }
48✔
523

54✔
524
      const options: { highlight?: boolean; } = data.markdown || {};
54✔
525
      if (!config.syntax_highlighter) options.highlight = null;
54!
526

54✔
527
      ctx.log.debug('Rendering post: %s', magenta(source));
54✔
528
      // Render with markdown or other renderer
54✔
529
      return ctx.render.render({
54✔
530
        text: data.content,
54✔
531
        path: source,
54✔
532
        engine: data.engine,
54✔
533
        toString: true,
54✔
534
        onRenderEnd(content) {
54✔
535
          // Replace cache data with real contents
54✔
536
          data.content = cacheObj.restoreAllSwigTags(content);
54✔
537

54✔
538
          // Return content after replace the placeholders
54✔
539
          if (disableNunjucks) return data.content;
54✔
540

50✔
541
          // Render with Nunjucks
50✔
542
          return tag.render(data.content, data);
50✔
543
        }
50✔
544
      }, options);
54✔
545
    }).then(content => {
54✔
546
      data.content = cacheObj.restoreCodeBlocks(content);
53✔
547

53✔
548
      // Run "after_post_render" filters
53✔
549
      return ctx.execFilter('after_post_render', data, { context: ctx });
53✔
550
    }).asCallback(callback);
54✔
551
  }
54✔
552
}
1✔
553

1✔
554
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