Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/hexo/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,9 @@ class Post {
return readFile(src);
}).then(content => {
// Create post
Object.assign(data, yfmParse(content));
Object.assign(data, yfmParse(content, {
defaultTimeZone: config.timezone
}));
data.content = data._content;
data._content = undefined;

Expand Down
6 changes: 5 additions & 1 deletion lib/hexo/validate_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ export = (ctx: Hexo): void => {
if (!config.timezone) {
log.warn('No timezone setting detected! Using LocalTimeZone as the default timezone.');
log.warn('This behavior will be changed to UTC in the next major version. Please set timezone explicitly (e.g. LocalTimeZone or America/New_York) in _config.yml to avoid this warning.');
config.timezone = moment.tz.guess();
} else if (config.timezone.toLowerCase() === 'localtimezone') {
config.timezone = moment.tz.guess();
} else {
const configTimezone = moment.tz.zone(config.timezone);
if (!configTimezone) {
log.warn(
`Invalid timezone setting detected! "${config.timezone}" is not a valid timezone.`
`Invalid timezone setting detected! "${config.timezone}" is not a valid timezone. Using the default timezone UTC.`
);
config.timezone = 'UTC';
} else {
const machineTimezone = moment.tz.guess();
if (configTimezone.name !== machineTimezone) {
Expand Down
16 changes: 12 additions & 4 deletions lib/models/types/moment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,18 @@ class SchemaTypeMoment extends warehouse.SchemaType<moment.Moment> {
}

function toMoment(value) {
// FIXME: Something is wrong when using a moment instance. I try to get the
// original date object and create a new moment object again.
if (moment.isMoment(value)) return moment((value as any)._d);
return moment(value);
// value passed here is a moment-like instance
// but it's a plain object that methods such as isValid are removed
// moment.isMoment is judged on its property but not constructor
// so the plain object still passes the moment.isMoment check

// hexo-front-matter now always returns date in UTC
// See https://github.com/hexojs/hexo-front-matter/pull/146
// We shall specify the timezone UTC here
// Otherwise, `moment()` set the timezone according to the $TZ on the machine
// Which still cause confusion
if (moment.isMoment(value)) return moment((value as any)._d).tz('UTC');
return moment(value).tz('UTC');
}

export = SchemaTypeMoment;
10 changes: 7 additions & 3 deletions lib/plugins/processor/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ function processPage(ctx: Hexo, file: _File) {
file.stat(),
file.read()
]).spread((stats: Stats, content: string) => {
const data: PageSchema = yfm(content);
const data: PageSchema = yfm(content, {
defaultTimeZone: config.timezone
});
const dateParsedWithTimezone = data.date instanceof Date;
const updatedParsedWithTimezone = data.updated instanceof Date;
const output = ctx.render.getOutput(path);

data.source = path;
Expand All @@ -62,15 +66,15 @@ function processPage(ctx: Hexo, file: _File) {
data.date = toDate(data.date) as any;

if (data.date) {
if (timezone) data.date = adjustDateForTimezone(data.date, timezone) as any;
if (timezone && !dateParsedWithTimezone) data.date = adjustDateForTimezone(data.date, timezone) as any;
} else {
data.date = stats.ctime as any;
}

data.updated = toDate(data.updated) as any;

if (data.updated) {
if (timezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any;
if (timezone && !updatedParsedWithTimezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any;
} else if (updated_option === 'date') {
data.updated = data.date;
} else if (updated_option === 'empty') {
Expand Down
13 changes: 10 additions & 3 deletions lib/plugins/processor/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ export {isTmpFile};
export {isHiddenFile};
export {isExcludedFile};

// This function is used by `asset.ts` and `post.ts`
// To handle dates like `date: Apr 24 2014` in front-matter
export function toDate(date?: string | number | Date | moment.Moment): Date | undefined | moment.Moment {
if (!date || moment.isMoment(date)) return date as any;

if (!(date instanceof Date)) {
// hexo-front-matter now always returns date in UTC
// but `new Date()` uses local timezone by default
// We have to reset offset
// to make the behavior consistent with hexo-front-matter
date = new Date(date);
// Adjust for local timezone offset to ensure UTC consistency
date = new Date(date.getTime() - (date.getTimezoneOffset() * DURATION_MINUTE));
}

if (isNaN(date.getTime())) return;
Expand All @@ -43,12 +51,11 @@ export function toDate(date?: string | number | Date | moment.Moment): Date | un
export function adjustDateForTimezone(date: Date | moment.Moment, timezone: string) {
if (moment.isMoment(date)) date = date.toDate();

const offset = date.getTimezoneOffset();
const ms = date.getTime();
const target = moment.tz.zone(timezone).utcOffset(ms);
const diff = (offset - target) * DURATION_MINUTE;
const diff = target * DURATION_MINUTE;

return new Date(ms - diff);
return new Date(ms + diff);
}

export {isMatch};
20 changes: 15 additions & 5 deletions lib/plugins/processor/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ function processPost(ctx: Hexo, file: _File) {
file.stat(),
file.read()
]).spread((stats: Stats, content: string) => {
const data: PostSchema = yfm(content);
const data: PostSchema = yfm(content, {
defaultTimeZone: config.timezone
});
const dateParsedWithTimezone = data.date instanceof Date;
const updatedParsedWithTimezone = data.updated instanceof Date;
const info = parseFilename(config.new_post_name, path);
const keys = Object.keys(info);

Expand All @@ -119,25 +123,31 @@ function processPost(ctx: Hexo, file: _File) {
}

if (data.date) {
// hexo-front-matter now always returns date in UTC
// See https://github.com/hexojs/hexo-front-matter/pull/146
data.date = toDate(data.date) as any;
} else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) {
data.date = new Date(
// Date parsed from permalink should also use UTC
// to make the behavior consistent with hexo-front-matter
// It will be corrected by invoking adjustDateForTimezone later
data.date = new Date(Date.UTC(
info.year,
parseInt(info.month || info.i_month, 10) - 1,
parseInt(info.day || info.i_day, 10)
Comment thread
stevenjoezhang marked this conversation as resolved.
) as any;
)) as any;
}

// Convert date and updated time from UTC to local time (based on timezone in Hexo config)
if (data.date) {
if (timezone) data.date = adjustDateForTimezone(data.date, timezone) as any;
if (timezone && !dateParsedWithTimezone) data.date = adjustDateForTimezone(data.date, timezone) as any;
} else {
data.date = stats.birthtime as any;
}

data.updated = toDate(data.updated) as any;

if (data.updated) {
if (timezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any;
if (timezone && !updatedParsedWithTimezone) data.updated = adjustDateForTimezone(data.updated, timezone) as any;
} else if (updated_option === 'date') {
data.updated = data.date;
} else if (updated_option === 'empty') {
Expand Down
38 changes: 30 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"fast-archy": "^1.0.0",
"fast-text-table": "^1.0.1",
"hexo-cli": "^4.3.2",
"hexo-front-matter": "^4.2.1",
"hexo-front-matter": "^5.0.0",
"hexo-fs": "^5.0.0",
"hexo-i18n": "^2.0.0",
"hexo-log": "^4.1.0",
Expand Down
2 changes: 1 addition & 1 deletion test/scripts/console/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ describe('new', () => {
const date = moment(now);
const path = join(hexo.source_dir, '_posts', 'Hello-World.md');
const body = [
'title: \'\'\'Hello\'\' World\'',
'title: "\'Hello\' World"',
'foo: bar',
'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),
'tags:',
Expand Down
29 changes: 12 additions & 17 deletions test/scripts/filters/post_permalink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('post_permalink', () => {
const apost = await Post.insert({
source: 'foo.md',
slug: 'foo',
date: moment('2014-01-02')
date: moment.utc('2014-01-02')
});
const id = apost._id;
await apost.setCategories(['foo', 'bar']);
Expand Down Expand Up @@ -77,7 +77,7 @@ describe('post_permalink', () => {
source: 'sub/2015-05-06-my-new-post.md',
slug: '2015-05-06-my-new-post',
title: 'My New Post',
date: moment('2015-05-06')
date: moment.utc('2015-05-06')
});
postPermalink(post).should.eql('2015/05/06/my-new-post/');
Post.removeById(post._id);
Expand All @@ -90,7 +90,7 @@ describe('post_permalink', () => {
source: 'sub/2015-05-06-my-new-post.md',
slug: '2015-05-06-my-new-post',
title: 'My New Post',
date: moment('2015-05-06 12:13:14')
date: moment.utc('2015-05-06 12:13:14')
});
postPermalink(post).should.eql('2015/05/06/12/13/14/my-new-post/');
Post.removeById(post._id);
Expand All @@ -100,8 +100,8 @@ describe('post_permalink', () => {
hexo.config.permalink = ':timestamp/:slug';
const timestamp = '1736401514';
const dates = [
moment('2025-01-09 05:45:14Z'),
moment('2025-01-08 22:45:14-07')
moment.utc('2025-01-09 05:45:14Z'),
moment.utc('2025-01-08 22:45:14-07')
];
const posts = await Post.insert(
dates.map((date, idx) => {
Expand All @@ -126,7 +126,7 @@ describe('post_permalink', () => {
source: 'sub/2015-05-06-my-new-post.md',
slug: '2015-05-06-my-new-post',
title: 'My New Post',
date: moment('2015-05-06')
date: moment.utc('2015-05-06')
});
postPermalink(post).should.eql('2015/05/06/00/00/00/my-new-post/');
Post.removeById(post._id);
Expand Down Expand Up @@ -180,14 +180,12 @@ describe('post_permalink', () => {
source: 'my-new-post.md',
slug: 'hexo/permalink-test',
__permalink: 'hexo/permalink-test',
title: 'Permalink Test',
date: moment('2014-01-02')
title: 'Permalink Test'
}, {
source: 'another-new-post.md',
slug: '/hexo-hexo/permalink-test-2',
__permalink: '/hexo-hexo/permalink-test-2',
title: 'Permalink Test',
date: moment('2014-01-02')
title: 'Permalink Test'
}]);

postPermalink(posts[0]).should.eql('/hexo/permalink-test');
Expand All @@ -203,7 +201,7 @@ describe('post_permalink', () => {
const post = await Post.insert({
source: 'foo.md',
slug: 'foo',
date: moment('2014-01-02')
date: moment.utc('2014-01-02')
});

postPermalink(post).should.eql('2014/01/02/foo/');
Expand All @@ -220,20 +218,17 @@ describe('post_permalink', () => {
source: 'my-new-post.md',
slug: 'hexo/permalink-test',
__permalink: 'hexo/permalink-test',
title: 'Permalink Test',
date: moment('2014-01-02')
title: 'Permalink Test'
}, {
source: 'another-new-post.md',
slug: '/hexo-hexo/permalink-test-2',
__permalink: '/hexo-hexo/permalink-test-2/',
title: 'Permalink Test',
date: moment('2014-01-02')
title: 'Permalink Test'
}, {
source: 'another-another-new-post.md',
slug: '/hexo-hexo/permalink-test-3',
__permalink: '/hexo-hexo/permalink-test-3.html',
title: 'Permalink Test',
date: moment('2014-01-02')
title: 'Permalink Test'
}]);

postPermalink(posts[0]).should.eql('/hexo/permalink-test/');
Expand Down
10 changes: 5 additions & 5 deletions test/scripts/models/moment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ describe('SchemaTypeMoment', () => {
const type = new SchemaTypeMoment('test');

it('cast()', () => {
type.cast(1e8).should.eql(moment(1e8));
type.cast(new Date(2014, 1, 1)).should.eql(moment(new Date(2014, 1, 1)));
type.cast('2014-11-03T07:45:41.237Z').should.eql(moment('2014-11-03T07:45:41.237Z'));
type.cast(moment(1e8)).valueOf().should.eql(1e8);
type.cast(1e8).should.eql(moment.utc(1e8).tz('UTC'));
type.cast(new Date(2014, 1, 1)).should.eql(moment.utc(new Date(2014, 1, 1)).tz('UTC'));
type.cast('2014-11-03T07:45:41.237Z').should.eql(moment.utc('2014-11-03T07:45:41.237Z').tz('UTC'));
type.cast(moment.utc(1e8)).valueOf().should.eql(1e8);
});

it('cast() - default', () => {
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('SchemaTypeMoment', () => {
});

it('parse()', () => {
type.parse('2014-11-03T07:45:41.237Z')!.should.eql(moment('2014-11-03T07:45:41.237Z'));
type.parse('2014-11-03T07:45:41.237Z')!.should.eql(moment.utc('2014-11-03T07:45:41.237Z').tz('UTC'));
should.not.exist(type.parse());
});

Expand Down
Loading
Loading