You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
7.3 KiB
291 lines
7.3 KiB
'use strict'; |
|
|
|
const { toDate, timezone, isExcludedFile, isTmpFile, isHiddenFile, isMatch } = require('./common'); |
|
const Promise = require('bluebird'); |
|
const { parse: yfm } = require('hexo-front-matter'); |
|
const { extname, join } = require('path'); |
|
const { stat, listDir } = require('hexo-fs'); |
|
const { slugize, Pattern, Permalink } = require('hexo-util'); |
|
const { magenta } = require('picocolors'); |
|
|
|
const postDir = '_posts/'; |
|
const draftDir = '_drafts/'; |
|
let permalink; |
|
|
|
const preservedKeys = { |
|
title: true, |
|
year: true, |
|
month: true, |
|
day: true, |
|
i_month: true, |
|
i_day: true, |
|
hash: true |
|
}; |
|
|
|
module.exports = ctx => { |
|
return { |
|
pattern: new Pattern(path => { |
|
if (isTmpFile(path)) return; |
|
|
|
let result; |
|
|
|
if (path.startsWith(postDir)) { |
|
result = { |
|
published: true, |
|
path: path.substring(postDir.length) |
|
}; |
|
} else if (path.startsWith(draftDir)) { |
|
result = { |
|
published: false, |
|
path: path.substring(draftDir.length) |
|
}; |
|
} |
|
|
|
if (!result || isHiddenFile(result.path)) return; |
|
|
|
// checks only if there is a renderer for the file type or if is included in skip_render |
|
result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render); |
|
|
|
// if post_asset_folder is set, restrict renderable files to default file extension |
|
if (result.renderable && ctx.config.post_asset_folder) { |
|
result.renderable = (extname(ctx.config.new_post_name) === extname(path)); |
|
} |
|
|
|
return result; |
|
}), |
|
|
|
process: function postProcessor(file) { |
|
if (file.params.renderable) { |
|
return processPost(ctx, file); |
|
} else if (ctx.config.post_asset_folder) { |
|
return processAsset(ctx, file); |
|
} |
|
} |
|
}; |
|
}; |
|
|
|
function processPost(ctx, file) { |
|
const Post = ctx.model('Post'); |
|
const { path } = file.params; |
|
const doc = Post.findOne({source: file.path}); |
|
const { config } = ctx; |
|
const { timezone: timezoneCfg } = config; |
|
// Deprecated: use_date_for_updated will be removed in future |
|
const updated_option = config.use_date_for_updated === true ? 'date' : config.updated_option; |
|
let categories, tags; |
|
|
|
if (file.type === 'skip' && doc) { |
|
return; |
|
} |
|
|
|
if (file.type === 'delete') { |
|
if (doc) { |
|
return doc.remove(); |
|
} |
|
|
|
return; |
|
} |
|
|
|
return Promise.all([ |
|
file.stat(), |
|
file.read() |
|
]).spread((stats, content) => { |
|
const data = yfm(content); |
|
const info = parseFilename(config.new_post_name, path); |
|
const keys = Object.keys(info); |
|
|
|
data.source = file.path; |
|
data.raw = content; |
|
data.slug = info.title; |
|
|
|
if (file.params.published) { |
|
if (!Object.prototype.hasOwnProperty.call(data, 'published')) data.published = true; |
|
} else { |
|
data.published = false; |
|
} |
|
|
|
for (let i = 0, len = keys.length; i < len; i++) { |
|
const key = keys[i]; |
|
if (!preservedKeys[key]) data[key] = info[key]; |
|
} |
|
|
|
if (data.date) { |
|
data.date = toDate(data.date); |
|
} else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) { |
|
data.date = new Date( |
|
info.year, |
|
parseInt(info.month || info.i_month, 10) - 1, |
|
parseInt(info.day || info.i_day, 10) |
|
); |
|
} |
|
|
|
if (data.date) { |
|
if (timezoneCfg) data.date = timezone(data.date, timezoneCfg); |
|
} else { |
|
data.date = stats.birthtime; |
|
} |
|
|
|
data.updated = toDate(data.updated); |
|
|
|
if (data.updated) { |
|
if (timezoneCfg) data.updated = timezone(data.updated, timezoneCfg); |
|
} else if (updated_option === 'date') { |
|
data.updated = data.date; |
|
} else if (updated_option === 'empty') { |
|
data.updated = undefined; |
|
} else { |
|
data.updated = stats.mtime; |
|
} |
|
|
|
if (data.category && !data.categories) { |
|
data.categories = data.category; |
|
data.category = undefined; |
|
} |
|
|
|
if (data.tag && !data.tags) { |
|
data.tags = data.tag; |
|
data.tag = undefined; |
|
} |
|
|
|
categories = data.categories || []; |
|
tags = data.tags || []; |
|
|
|
if (!Array.isArray(categories)) categories = [categories]; |
|
if (!Array.isArray(tags)) tags = [tags]; |
|
|
|
if (data.photo && !data.photos) { |
|
data.photos = data.photo; |
|
data.photo = undefined; |
|
} |
|
|
|
if (data.photos && !Array.isArray(data.photos)) { |
|
data.photos = [data.photos]; |
|
} |
|
|
|
if (data.link && !data.title) { |
|
data.title = data.link.replace(/^https?:\/\/|\/$/g, ''); |
|
} |
|
|
|
if (data.permalink) { |
|
data.__permalink = data.permalink; |
|
data.permalink = undefined; |
|
} |
|
|
|
if (doc) { |
|
if (file.type !== 'update') { |
|
ctx.log.warn(`Trying to "create" ${magenta(file.path)}, but the file already exists!`); |
|
} |
|
return doc.replace(data); |
|
} |
|
|
|
return Post.insert(data); |
|
}).then(doc => Promise.all([ |
|
doc.setCategories(categories), |
|
doc.setTags(tags), |
|
scanAssetDir(ctx, doc) |
|
])); |
|
} |
|
|
|
function parseFilename(config, path) { |
|
config = config.substring(0, config.length - extname(config).length); |
|
path = path.substring(0, path.length - extname(path).length); |
|
|
|
if (!permalink || permalink.rule !== config) { |
|
permalink = new Permalink(config, { |
|
segments: { |
|
year: /(\d{4})/, |
|
month: /(\d{2})/, |
|
day: /(\d{2})/, |
|
i_month: /(\d{1,2})/, |
|
i_day: /(\d{1,2})/, |
|
hash: /([0-9a-f]{12})/ |
|
} |
|
}); |
|
} |
|
|
|
const data = permalink.parse(path); |
|
|
|
if (data) { |
|
return data; |
|
} |
|
|
|
return { |
|
title: slugize(path) |
|
}; |
|
} |
|
|
|
function scanAssetDir(ctx, post) { |
|
if (!ctx.config.post_asset_folder) return; |
|
|
|
const assetDir = post.asset_dir; |
|
const baseDir = ctx.base_dir; |
|
const baseDirLength = baseDir.length; |
|
const PostAsset = ctx.model('PostAsset'); |
|
|
|
return stat(assetDir).then(stats => { |
|
if (!stats.isDirectory()) return []; |
|
|
|
return listDir(assetDir); |
|
}).catch(err => { |
|
if (err && err.code === 'ENOENT') return []; |
|
throw err; |
|
}).filter(item => !isExcludedFile(item, ctx.config)).map(item => { |
|
const id = join(assetDir, item).substring(baseDirLength).replace(/\\/g, '/'); |
|
const asset = PostAsset.findById(id); |
|
|
|
if (shouldSkipAsset(ctx, post, asset)) return undefined; |
|
|
|
return PostAsset.save({ |
|
_id: id, |
|
post: post._id, |
|
slug: item, |
|
modified: true |
|
}); |
|
}); |
|
} |
|
|
|
function shouldSkipAsset(ctx, post, asset) { |
|
if (!ctx._showDrafts()) { |
|
if (post.published === false && asset) { |
|
// delete existing draft assets if draft posts are hidden |
|
asset.remove(); |
|
} |
|
if (post.published === false) { |
|
// skip draft assets if draft posts are hidden |
|
return true; |
|
} |
|
} |
|
|
|
return asset !== undefined; // skip already existing assets |
|
} |
|
|
|
function processAsset(ctx, file) { |
|
const PostAsset = ctx.model('PostAsset'); |
|
const Post = ctx.model('Post'); |
|
const id = file.source.substring(ctx.base_dir.length).replace(/\\/g, '/'); |
|
const doc = PostAsset.findById(id); |
|
|
|
if (file.type === 'delete') { |
|
if (doc) { |
|
return doc.remove(); |
|
} |
|
|
|
return; |
|
} |
|
|
|
// TODO: Better post searching |
|
const post = Post.toArray().find(post => file.source.startsWith(post.asset_dir)); |
|
if (post != null && (post.published || ctx._showDrafts())) { |
|
return PostAsset.save({ |
|
_id: id, |
|
slug: file.source.substring(post.asset_dir.length), |
|
post: post._id, |
|
modified: file.type !== 'skip', |
|
renderable: file.params.renderable |
|
}); |
|
} |
|
|
|
if (doc) { |
|
return doc.remove(); |
|
} |
|
}
|
|
|