Skip to content
Open
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 @@ -458,7 +458,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
25 changes: 24 additions & 1 deletion lib/hexo/validate_config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'assert';
import moment from 'moment-timezone';
import type Hexo from './index';

export = (ctx: Hexo): void => {
Expand All @@ -24,5 +25,27 @@ export = (ctx: Hexo): void => {
if (config.root.trim().length <= 0) {
throw new TypeError('Invalid config detected: "root" should not be empty!');
}
};

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. Using the default timezone UTC.`
);
config.timezone = 'UTC';
} else {
const machineTimezone = moment.tz.guess();
if (configTimezone.name !== machineTimezone) {
log.warn(
`The timezone "${config.timezone}" setting is different from your machine timezone "${machineTimezone}". Make sure this is intended.`
);
}
}
}
};
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;
4 changes: 3 additions & 1 deletion lib/plugins/processor/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ 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 output = ctx.render.getOutput(path);

data.source = path;
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};
14 changes: 11 additions & 3 deletions lib/plugins/processor/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ 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 info = parseFilename(config.new_post_name, path);
const keys = Object.keys(info);

Expand All @@ -119,15 +121,21 @@ 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)
) 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;
} else {
Expand Down
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
7 changes: 4 additions & 3 deletions test/scripts/processors/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import moment from 'moment';
import { isTmpFile, isHiddenFile, toDate, adjustDateForTimezone, isMatch } from '../../../lib/plugins/processor/common';
import chai from 'chai';
const should = chai.should();
const DURATION_MINUTE = 1000 * 60;

describe('common', () => {
it('isTmpFile()', () => {
Expand All @@ -21,13 +22,13 @@ describe('common', () => {
it('toDate()', () => {
const m = moment();
const d = new Date();
const diff = d.getTimezoneOffset() * DURATION_MINUTE;

should.not.exist(toDate());
toDate(m)!.should.eql(m);
toDate(d)!.should.eql(d);
toDate(1e8)!.should.eql(new Date(1e8));
toDate('2014-04-25T01:32:21.196Z')!.should.eql(new Date('2014-04-25T01:32:21.196Z'));
toDate('Apr 24 2014')!.should.eql(new Date(2014, 3, 24));
toDate(1e8)!.should.eql(new Date(1e8 - diff));
toDate('Apr 24 2014')!.should.eql(new Date(new Date(2014, 3, 24).getTime() - diff));
should.not.exist(toDate('foo'));
});

Expand Down
Loading