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

hexojs / hexo / 12731481067

12 Jan 2025 07:18AM UTC coverage: 99.491% (+0.009%) from 99.482%
12731481067

Pull #5483

github

web-flow
Merge 74ab0906d into bcfb0301d
Pull Request #5483: WIP: refactor: refactor types

2305 of 2397 branches covered (96.16%)

374 of 375 new or added lines in 44 files covered. (99.73%)

2 existing lines in 1 file now uncovered.

9583 of 9632 relevant lines covered (99.49%)

51.54 hits per line

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

97.78
/lib/plugins/processor/post.ts
1
import { toDate, timezone, isExcludedFile, isTmpFile, isHiddenFile, isMatch } from './common';
1✔
2
import Promise from 'bluebird';
1✔
3
import { parse as yfm } from 'hexo-front-matter';
1✔
4
import { extname, join, posix, sep } from 'path';
1✔
5
import { stat, listDir } from 'hexo-fs';
1✔
6
import { slugize, Pattern, Permalink } from 'hexo-util';
1✔
7
import { magenta } from 'picocolors';
1✔
8
import type { _File } from '../../box';
1✔
9
import type Hexo from '../../hexo';
1✔
10
import type { Stats } from 'fs';
1✔
11
import { PostSchema } from '../../types';
1✔
12
import type Document from 'warehouse/dist/document';
1✔
13

1✔
14
const postDir = '_posts/';
1✔
15
const draftDir = '_drafts/';
1✔
16
let permalink;
1✔
17

1✔
18
const preservedKeys = {
1✔
19
  title: true,
1✔
20
  year: true,
1✔
21
  month: true,
1✔
22
  day: true,
1✔
23
  i_month: true,
1✔
24
  i_day: true,
1✔
25
  hash: true
1✔
26
};
1✔
27

1✔
28
export = (ctx: Hexo) => {
1✔
29
  return {
70✔
30
    pattern: new Pattern(path => {
70✔
31
      if (isTmpFile(path)) return;
42✔
32

40✔
33
      let result;
40✔
34

40✔
35
      if (path.startsWith(postDir)) {
42✔
36
        result = {
13✔
37
          published: true,
13✔
38
          path: path.substring(postDir.length)
13✔
39
        };
13✔
40
      } else if (path.startsWith(draftDir)) {
42✔
41
        result = {
2✔
42
          published: false,
2✔
43
          path: path.substring(draftDir.length)
2✔
44
        };
2✔
45
      }
2✔
46

40✔
47
      if (!result || isHiddenFile(result.path)) return;
42✔
48

11✔
49
      // checks only if there is a renderer for the file type or if is included in skip_render
11✔
50
      result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render);
42✔
51

42✔
52
      // if post_asset_folder is set, restrict renderable files to default file extension
42✔
53
      if (result.renderable && ctx.config.post_asset_folder) {
42✔
54
        result.renderable = (extname(ctx.config.new_post_name) === extname(path));
3✔
55
      }
3✔
56

11✔
57
      return result;
11✔
58
    }),
70✔
59

70✔
60
    process: function postProcessor(file: _File) {
70✔
61
      if (file.params.renderable) {
46✔
62
        return processPost(ctx, file);
38✔
63
      } else if (ctx.config.post_asset_folder) {
46✔
64
        return processAsset(ctx, file);
7✔
65
      }
7✔
66
    }
46✔
67
  };
70✔
68
};
70✔
69

1✔
70
function processPost(ctx: Hexo, file: _File) {
38✔
71
  const Post = ctx.model('Post');
38✔
72
  const { path } = file.params;
38✔
73
  const doc = Post.findOne({source: file.path});
38✔
74
  const { config } = ctx;
38✔
75
  const { timezone: timezoneCfg, updated_option, use_slug_as_post_title } = config;
38✔
76

38✔
77
  let categories, tags;
38✔
78

38✔
79
  if (file.type === 'skip' && doc) {
38✔
80
    return;
1✔
81
  }
1✔
82

37✔
83
  if (file.type === 'delete') {
38✔
84
    if (doc) {
3✔
85
      return doc.remove();
1✔
86
    }
1✔
87

2✔
88
    return;
2✔
89
  }
2✔
90

34✔
91
  return Promise.all([
34✔
92
    file.stat(),
34✔
93
    file.read()
34✔
94
  ]).spread((stats: Stats, content: string) => {
34✔
95
    const data = yfm(content);
34✔
96
    const info = parseFilename(config.new_post_name, path);
34✔
97
    const keys = Object.keys(info);
34✔
98

34✔
99
    data.source = file.path;
34✔
100
    data.raw = content;
34✔
101
    data.slug = info.title;
34✔
102

34✔
103
    if (file.params.published) {
34✔
104
      if (!Object.prototype.hasOwnProperty.call(data, 'published')) data.published = true;
33✔
105
    } else {
34✔
106
      data.published = false;
1✔
107
    }
1✔
108

34✔
109
    for (let i = 0, len = keys.length; i < len; i++) {
34✔
110
      const key = keys[i];
44✔
111
      if (!preservedKeys[key]) data[key] = info[key];
44✔
112
    }
44✔
113

34✔
114
    // use `slug` as `title` of post when `title` is not specified.
34✔
115
    // https://github.com/hexojs/hexo/issues/5372
34✔
116
    if (use_slug_as_post_title && !('title' in data)) {
34✔
117
      data.title = info.title;
1✔
118
    }
1✔
119

34✔
120
    if (data.date) {
34✔
121
      data.date = toDate(data.date);
6✔
122
    } else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) {
34!
123
      data.date = new Date(
3✔
124
        info.year,
3✔
125
        parseInt(info.month || info.i_month, 10) - 1,
3!
126
        parseInt(info.day || info.i_day, 10)
3!
127
      );
3✔
128
    }
3✔
129

34✔
130
    if (data.date) {
34✔
131
      if (timezoneCfg) data.date = timezone(data.date, timezoneCfg);
8✔
132
    } else {
34✔
133
      data.date = stats.birthtime;
26✔
134
    }
26✔
135

34✔
136
    data.updated = toDate(data.updated);
34✔
137

34✔
138
    if (data.updated) {
34✔
139
      if (timezoneCfg) data.updated = timezone(data.updated, timezoneCfg);
3✔
140
    } else if (updated_option === 'date') {
34✔
141
      data.updated = data.date;
1✔
142
    } else if (updated_option === 'empty') {
31✔
143
      data.updated = undefined;
1✔
144
    } else {
30✔
145
      data.updated = stats.mtime;
29✔
146
    }
29✔
147

34✔
148
    if (data.category && !data.categories) {
34✔
149
      data.categories = data.category;
1✔
150
      data.category = undefined;
1✔
151
    }
1✔
152

34✔
153
    if (data.tag && !data.tags) {
34✔
154
      data.tags = data.tag;
1✔
155
      data.tag = undefined;
1✔
156
    }
1✔
157

34✔
158
    categories = data.categories || [];
34✔
159
    tags = data.tags || [];
34✔
160

34✔
161
    if (!Array.isArray(categories)) categories = [categories];
34✔
162
    if (!Array.isArray(tags)) tags = [tags];
34✔
163

34✔
164
    if (data.photo && !data.photos) {
34✔
165
      data.photos = data.photo;
1✔
166
      data.photo = undefined;
1✔
167
    }
1✔
168

34✔
169
    if (data.photos && !Array.isArray(data.photos)) {
34✔
170
      data.photos = [data.photos];
1✔
171
    }
1✔
172

34✔
173
    if (data.permalink) {
34✔
174
      data.__permalink = data.permalink;
1✔
175
      data.permalink = undefined;
1✔
176
    }
1✔
177

34✔
178
    if (doc) {
34✔
179
      if (file.type !== 'update') {
3✔
180
        ctx.log.warn(`Trying to "create" ${magenta(file.path)}, but the file already exists!`);
2✔
181
      }
2✔
182
      return doc.replace(data);
3✔
183
    }
3✔
184

31✔
185
    return Post.insert(data);
31✔
186
  }).then(doc => Promise.all([
34✔
187
    doc.setCategories(categories),
34✔
188
    doc.setTags(tags),
34✔
189
    scanAssetDir(ctx, doc)
34✔
190
  ]));
34✔
191
}
34✔
192

1✔
193
function parseFilename(config: string, path: string) {
34✔
194
  config = config.substring(0, config.length - extname(config).length);
34✔
195
  path = path.substring(0, path.length - extname(path).length);
34✔
196

34✔
197
  if (!permalink || permalink.rule !== config) {
34✔
198
    permalink = new Permalink(config, {
8✔
199
      segments: {
8✔
200
        year: /(\d{4})/,
8✔
201
        month: /(\d{2})/,
8✔
202
        day: /(\d{2})/,
8✔
203
        i_month: /(\d{1,2})/,
8✔
204
        i_day: /(\d{1,2})/,
8✔
205
        hash: /([0-9a-f]{12})/
8✔
206
      }
8✔
207
    });
8✔
208
  }
8✔
209

34✔
210
  const data = permalink.parse(path);
34✔
211

34✔
212
  if (data) {
34✔
213
    if (data.title !== undefined) {
33✔
214
      return data;
32✔
215
    }
32✔
216
    return Object.assign(data, {
1✔
217
      title: slugize(path)
1✔
218
    });
1✔
219
  }
1✔
220

1✔
221
  return {
1✔
222
    title: slugize(path)
1✔
223
  };
1✔
224
}
1✔
225

1✔
226
function scanAssetDir(ctx: Hexo, post) {
34✔
227
  if (!ctx.config.post_asset_folder) return;
34✔
228

7✔
229
  const assetDir = post.asset_dir;
7✔
230
  const baseDir = ctx.base_dir;
7✔
231
  const sourceDir = ctx.config.source_dir;
7✔
232
  const baseDirLength = baseDir.length;
7✔
233
  const sourceDirLength = sourceDir.length;
7✔
234
  const PostAsset = ctx.model('PostAsset');
7✔
235

7✔
236
  return stat(assetDir).then(stats => {
7✔
237
    if (!stats.isDirectory()) return [];
7!
238

7✔
239
    return listDir(assetDir);
7✔
240
  }).catch(err => {
7✔
UNCOV
241
    if (err && err.code === 'ENOENT') return [];
×
242
    throw err;
×
243
  }).filter(item => !isExcludedFile(item, ctx.config)).map(item => {
7✔
244
    const id = join(assetDir, item).substring(baseDirLength).replace(/\\/g, '/');
8✔
245
    const renderablePath = id.substring(sourceDirLength + 1);
8✔
246
    const asset = PostAsset.findById(id);
8✔
247

8✔
248
    if (shouldSkipAsset(ctx, post, asset)) return undefined;
8✔
249

5✔
250
    return PostAsset.save({
5✔
251
      _id: id,
5✔
252
      post: post._id,
5✔
253
      slug: item,
5✔
254
      modified: true,
5✔
255
      renderable: ctx.render.isRenderable(renderablePath) && !isMatch(renderablePath, ctx.config.skip_render)
8✔
256
    });
8✔
257
  });
7✔
258
}
7✔
259

1✔
260
function shouldSkipAsset(ctx: Hexo, post, asset) {
8✔
261
  if (!ctx._showDrafts()) {
8✔
262
    if (post.published === false && asset) {
7✔
263
      // delete existing draft assets if draft posts are hidden
1✔
264
      asset.remove();
1✔
265
    }
1✔
266
    if (post.published === false) {
7✔
267
      // skip draft assets if draft posts are hidden
3✔
268
      return true;
3✔
269
    }
3✔
270
  }
7✔
271

5✔
272
  return asset !== undefined; // skip already existing assets
5✔
273
}
5✔
274

1✔
275
function processAsset(ctx: Hexo, file: _File) {
7✔
276
  const PostAsset = ctx.model('PostAsset');
7✔
277
  const Post = ctx.model('Post');
7✔
278
  const id = file.source.substring(ctx.base_dir.length).replace(/\\/g, '/');
7✔
279
  const postAsset = PostAsset.findById(id);
7✔
280

7✔
281
  if (file.type === 'delete' || Post.length === 0) {
7✔
282
    if (postAsset) {
4✔
283
      return postAsset.remove();
2✔
284
    }
2✔
285
    return;
2✔
286
  }
2✔
287

3✔
288
  const savePostAsset = (post: Document<PostSchema>) => {
3✔
289
    return PostAsset.save({
3✔
290
      _id: id,
3✔
291
      slug: file.source.substring(post.asset_dir.length),
3✔
292
      post: post._id,
3✔
293
      modified: file.type !== 'skip',
3✔
294
      renderable: file.params.renderable
3✔
295
    });
3✔
296
  };
3✔
297

3✔
298
  if (postAsset) {
7✔
299
    // `postAsset.post` is `Post.id`.
2✔
300
    const post = Post.findById(postAsset.post);
2✔
301
    if (post != null && (post.published || ctx._showDrafts())) {
2!
302
      return savePostAsset(post);
2✔
303
    }
2✔
304
  }
2✔
305

1✔
306
  const assetDir = id.slice(0, id.lastIndexOf(sep));
1✔
307
  const post = Post.findOne(p => p.asset_dir.endsWith(posix.join(assetDir, '/')));
1✔
308
  if (post != null && (post.published || ctx._showDrafts())) {
7!
309
    return savePostAsset(post);
1✔
310
  }
1✔
UNCOV
311

×
312
  // NOTE: Probably, unreachable.
×
313
  if (postAsset) {
×
314
    return postAsset.remove();
×
315
  }
×
316
}
7✔
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

© 2026 Coveralls, Inc