Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance date formatting #428

Merged
merged 15 commits into from
Aug 17, 2024
Merged
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
18 changes: 18 additions & 0 deletions demo/yun/components/test/TestFormatDate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import { formatDate } from 'valaxy'
import type { FormatOptionsWithTZ } from 'date-fns-tz'

defineProps<{ date?: string | number | Date, format?: string, timezone?: string, options?: FormatOptionsWithTZ }>()
</script>

<template>
<div flex="~">
<time mr-4>
{{ formatDate(date ?? new Date(), format, timezone, options) }}
</time>

<code>
{{ format }}
</code>
</div>
</template>
69 changes: 69 additions & 0 deletions demo/yun/pages/posts/date.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,73 @@
title: Date test
date: 2023-07-19 18:55:53
# updated: 2023-07-19 18:55:53
toc: false
---

<TestFormatDate format="MMM d" />

<TestFormatDate format="MMM d, yyyy" />

<TestFormatDate date="2023-07-19T00:00:00+08:00" format="d MMM yyyy" />

<TestFormatDate date="1847-05-16T00:01:15.000Z" format="yyyy-MM-dd HH:mm:ss" />

<TestFormatDate format="EEEE, MMMM d, yyyy" />

<TestFormatDate format="EEEE, d MMMM yyyy" />

<TestFormatDate format="yyyyMMdd" />

<TestFormatDate format="yy/MM/dd" />

<TestFormatDate format="HH:mm:ss" />

<TestFormatDate format="h:mm a" />

<TestFormatDate format="hh 'o''clock' a, zzzz" />

<TestFormatDate format="K:mm a, z" />

<TestFormatDate date="2023-07-19T00:00:00+08:00" format="EEEE, d MMMM yyyy" />

<TestFormatDate id="text-time-format-1" date="2023-07-19T00:00:00+08:00" format="yyyyMMdd" :options="{ timeZone: 'Asia/Shanghai' }" />

<TestFormatDate :date="1722589089" format="yyyyMMdd" />

<TestFormatDate id="text-time-format-2" date="2021.3.1 12:00" format="yyyyMMddHHmmss" :options="{ timeZone: 'Asia/Shanghai' }" />

<TestFormatDate format="yyMMdd" />

<TestFormatDate date="2021/3/1 12:00" format="yyyy/MM/dd" />

<TestFormatDate id="text-time-format-3" date="2021-12-03 1:07:23" format="yyyy/MM/dd HH:mm" :options="{ timeZone: 'Asia/Shanghai' }" />

<TestFormatDate format="X" />

<TestFormatDate format="x" />

<TestFormatDate date="2023-07-19 10:55:53Z" format="yyyy-MM-dd'T'HH:mm:ssXXX" />

<TestFormatDate format="yyyy-MM-dd'T'HH:mm:ss.SSSXXX" />

<TestFormatDate :date="1722589089" format="T" />

<TestFormatDate format="R" />

<TestFormatDate format="do" />

<TestFormatDate id="text-time-zone-1" date="2004-06-16T00:00:00+08:00" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Europe/Berlin" :options="{ timeZone: 'Europe/Berlin' }" />

<TestFormatDate id="text-time-zone-2" date="2004-06-16 00:00:00" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Asia/Shanghai" :options="{ timeZone: 'Europe/Berlin' }" />

<TestFormatDate id="text-time-zone-3" date="2004-06-16 00:00:00" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Asia/Shanghai" :options="{ timeZone: 'Asia/Shanghai' }" />

<TestFormatDate id="text-time-zone-4" date="2004-06-16T00:00:00Z" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Asia/Shanghai" :options="{ timeZone: 'Europe/Berlin' }" />

<TestFormatDate id="text-time-zone-5" date="2004-06-16 00:00:00" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Asia/Shanghai" :options="{ timeZone: 'Asia/Bangkok' }" />

<TestFormatDate id="text-time-zone-6" date="2004-06-16 00:00:00" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Europe/Berlin" :options="{ timeZone: 'Asia/Shanghai' }" />

<TestFormatDate id="text-time-zone-7" date="2004-06-16 00:00:00" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Europe/Berlin" :options="{ timeZone: 'Europe/Berlin' }" />

<TestFormatDate id="text-time-zone-8" date="2004-06-16T00:00:00Z" format="yyyy-MM-dd HH:mm:ssxxx zzz" timezone="Europe/Berlin" :options="{ timeZone: 'Asia/Shanghai' }" />
1 change: 1 addition & 0 deletions demo/yun/site.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default defineSiteConfig({

lang: 'zh-CN',
title: 'Valaxy Theme Yun',
timezone: 'Asia/Shanghai',
url: 'https://yun.valaxy.site/',
author: {
avatar: 'https://www.yunyoujun.cn/images/avatar.jpg',
Expand Down
48 changes: 0 additions & 48 deletions docs/pages/faq/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,51 +71,3 @@ So you can create a new file named `.nojekyll` in the `public` folder of the pro
|-- public
| |-- .nojekyll
```

## 显示的文章创建/修改时间不正确 {lang="zh-CN"}

## The displayed article creation/modification time is incorrect {lang="en"}

::: zh-CN
根据[这份 `YAML` 规范](https://yaml.org/type/timestamp.html),符合 `ISO 8601` 标准的时间格式都会被解析为 `Date` 类型,且**不显式标注时区的时间戳都会作 UTC 处理**。

但是为了方便写作与从其他框架迁移,我们将未显式标注时区的时间戳解析为**系统时区**对应的时间(即 `2024-07-06 12:00:00` 在 `Asia/Shanghai` 下会解析 为 `2024-07-06T12:00:00+08:00`)。

无论如何,我们建议**显式**添加时区信息,例如:

```yaml
date: 2024-07-06 12:00:00 +8
```

如果你不喜欢显式标记,请**务必**在构建前指定系统时区:

```bash
sudo timedatectl set-timezone Asia/Shanghai
```

这样就能正确解析为 UTC+8 时区的 `2024-07-06 12:00:00`。

主题作者也能通过在 `scaffolds/post.md` 中使用 `date: <%=date%> +8` 来实现这一点。
:::

::: en
According to [this `YAML` specification](https://yaml.org/type/timestamp.html), time formats that conform to the `ISO 8601` standard will be parsed as `Date` type, and **timestamps without explicitly marked time zones will be treated as UTC**.

However, for the convenience of writing and migrating from other frameworks, we parse timestamps without explicitly marked timezones under system timezone (i.e., `2024-07-06 12:00:00` will be parsed as `2024-07-06T12:00:00+08:00` in `Asia/Shanghai`).

If you don't like explicit marking, **be sure** to specify the system time zone before building:

```bash
sudo timedatectl set-timezone Asia/Shanghai
```

Nevertheless, we recommend **explicitly** adding time zone information, for example:

```yaml
date: 2024-07-06 12:00:00 +8
```

This way, it can be correctly parsed as `2024-07-06 12:00:00` in the UTC+8 time zone.

Theme authors can also achieve this by using `date: <%=date%> +8` in `scaffolds/post.md`.
:::
30 changes: 30 additions & 0 deletions e2e/theme-yun/time.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, test } from '@playwright/test'
import { env } from '../env'

test.use({
baseURL: env['theme-yun'],
})

test.beforeEach(async ({ page }) => {
await page.goto('/posts/date')
})

test.describe('Frontmatter', () => {
test('time format validation', async ({ page }) => {
await expect(page.locator('#text-time-format-1 time')).toHaveText('20230719')
await expect(page.locator('#text-time-format-2 time')).toHaveText('20210301120000')
await expect(page.locator('#text-time-format-3 time')).toHaveText('2021/12/03 01:07')
})

test('timezone format validation', async ({ page }) => {
await expect(page.locator('#text-time-zone-1 time')).toHaveText('2004-06-15 18:00:00+02:00 GMT+2')
await expect(page.locator('#text-time-zone-2 time')).toHaveText('2004-06-15 18:00:00+02:00 GMT+2')
await expect(page.locator('#text-time-zone-3 time')).toHaveText('2004-06-16 00:00:00+08:00 GMT+8')
await expect(page.locator('#text-time-zone-4 time')).toHaveText('2004-06-16 02:00:00+02:00 GMT+2')
await expect(page.locator('#text-time-zone-5 time')).toHaveText('2004-06-15 23:00:00+07:00 GMT+7')
await expect(page.locator('#text-time-zone-6 time')).toHaveText('2004-06-16 06:00:00+08:00 GMT+8')
await expect(page.locator('#text-time-zone-6 time')).toHaveText('2004-06-16 06:00:00+08:00 GMT+8')
await expect(page.locator('#text-time-zone-7 time')).toHaveText('2004-06-16 00:00:00+02:00 GMT+2')
await expect(page.locator('#text-time-zone-8 time')).toHaveText('2004-06-16 08:00:00+08:00 GMT+8')
})
})
3 changes: 2 additions & 1 deletion packages/valaxy-theme-yun/components/YunPostMeta.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts" setup>
import type { Post } from 'valaxy'
import { formatDate, formatTimestamp, useSiteConfig } from 'valaxy'
import { formatDate, useSiteConfig } from 'valaxy'
import { useI18n } from 'vue-i18n'
import { formatTimestamp } from '../utils'

defineProps<{
// FrontMatter
Expand Down
9 changes: 9 additions & 0 deletions packages/valaxy-theme-yun/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatDate } from 'valaxy'
import noneImg from '../assets/images/none.jpg'

/**
Expand All @@ -9,3 +10,11 @@ export function onImgError(e: Event, defaultImg = noneImg) {
targetEl.setAttribute('data-src', targetEl.src)
targetEl.src = defaultImg
}

/**
* date-fns format date with 'yyyy-MM-dd HH:mm:ss'
* @param date
*/
export function formatTimestamp(date: string | number | Date): string {
return formatDate(date, 'yyyy-MM-dd HH:mm:ss')
}
68 changes: 56 additions & 12 deletions packages/valaxy/client/utils/time.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,65 @@
import { format } from 'date-fns'
import type { ToDateOptionsWithTZ } from 'date-fns-tz'
import { format as formatWithTZ, toZonedTime } from 'date-fns-tz'
import { format, toDate } from 'date-fns'
import { useSiteConfig } from 'valaxy'
import { useI18n } from 'vue-i18n'
import { DateTime } from 'luxon'
import type { Post } from '../../types'

const referenceDate = new Date(1986, 3 /* Apr */, 4, 10, 32, 0, 900)

/**
* date-fns format date
* @param date
* @param template
* format the date
* @param date the original date
* @param formatStr the string of tokens
* @param timezone the time zone of this local time, can be an offset or IANA time zone
* @param options the object with options. See [Options]{@link https://date-fns.org/docs/Options}
*/
export function formatDate(date: string | number | Date, template = 'yyyy-MM-dd') {
return format(date, template)
export function formatDate(date: string | number | Date, formatStr = 'yyyy-MM-dd', timezone?: string, options?: ToDateOptionsWithTZ): string {
const { locale } = useI18n()
const siteConfig = useSiteConfig()

const mergedOptions: ToDateOptionsWithTZ = Object.assign({ locale: { code: locale.value } }, options)
const clientTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone

try {
/**
* Format the timezone-less date to ISO. If none is specified, use the client's timezone.
* If the input date is already in ISO format, the timezone won't be applied.
*/
date = handleTimeWithZone(date, timezone || siteConfig.value.timezone || clientTimezone).toString()
// Convert to the client's timezone unless the user specifies otherwise
const zonedDate = toZonedTime(date, options?.timeZone || clientTimezone, mergedOptions)
// The format function will never change the underlying date
return formatWithTZ(zonedDate, formatStr, { timeZone: options?.timeZone })
}
catch (error) {
console.error(
'The date format provided is non-standard. The recommended format is \'yyyy-MM-dd HH:mm:ss\'',
'\nError formatting date:',
date.toString(),
error,
)
return format(referenceDate, formatStr)
}
}

/**
* date-fns format date with 'yyyy-MM-dd HH:mm:ss'
* @param date
*/
export function formatTimestamp(date: string | number | Date): string {
return format(date, 'yyyy-MM-dd HH:mm:ss')
function handleTimeWithZone(date: string | number | Date, timezone: string) {
if (typeof date !== 'string')
date = toDate(date).toISOString()

let dateTime = DateTime.fromISO(date, { setZone: true })

const toDateTime = (date: string, zone: string) => {
// Attempt to format the date using a function that handles non-ISO 8601 formats
const isoDate = format(date, 'yyyy-MM-dd\'T\'HH:mm:ss')
return DateTime.fromISO(isoDate, { zone })
}

if (!dateTime.isValid || !dateTime.zoneName)
dateTime = toDateTime(date, timezone)

return dateTime
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/valaxy/node/config/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const defaultSiteConfig: SiteConfig = {
url: '/',
lang: 'en',
languages: ['en', 'zh-CN'],
timezone: '',
title: 'Valaxy Blog',
description: 'A blog generated by Valaxy.',
subtitle: 'Next Generation Static Blog Framework.',
Expand Down
2 changes: 1 addition & 1 deletion packages/valaxy/node/modules/fuse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { cyan, dim } from 'picocolors'
import type { Argv } from 'yargs'

import type { FuseListItem } from 'valaxy/types'
import { matterOptions } from '../utils/matterOptions'
import { matterOptions } from '../plugins/markdown/transform/matter'
import { resolveOptions } from '../options'
import { setEnvProd } from '../utils/env'
import { commonOptions } from '../cli/options'
Expand Down
2 changes: 1 addition & 1 deletion packages/valaxy/node/modules/rss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Author, FeedOptions, Item } from 'feed'
import { Feed } from 'feed'
import consola from 'consola'
import { getCreatedTime, getUpdatedTime } from '../utils/date'
import { matterOptions } from '../utils/matterOptions'
import { matterOptions } from '../plugins/markdown/transform/matter'
import { type ResolvedValaxyOptions, resolveOptions } from '../options'
import { ensurePrefix, isExternal } from '../utils'
import { commonOptions } from '../cli/options'
Expand Down
14 changes: 1 addition & 13 deletions packages/valaxy/node/plugins/markdown/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import type { Plugin } from 'vite'
import Markdown from 'unplugin-vue-markdown/vite'

import type MarkdownIt from 'markdown-it'
import type { PageFrontMatter } from 'valaxy/types'
import { matterOptions } from '../../../utils/matterOptions'
import type { ResolvedValaxyOptions } from '../../../options'
import { highlight } from '../plugins/highlight'
import { defaultCodeTheme, setupMarkdownPlugins } from '../setup'
import { matterOptions } from './matter'
import { createTransformIncludes } from './include'
import { transformMermaid } from './mermaid'

Expand All @@ -26,17 +25,6 @@ export async function createMarkdownPlugin(
frontmatter: true,
exportFrontmatter: false,
frontmatterOptions: { grayMatterOptions: matterOptions },
frontmatterPreprocess(frontmatter, mdOptions, _, defaultHeadProcess) {
const fm = frontmatter as PageFrontMatter
if (fm.date)
fm.date = new Date(fm.date)
if (fm.updated)
fm.updated = new Date(fm.updated)
return {
head: defaultHeadProcess(frontmatter, mdOptions),
frontmatter,
}
},

// v-pre
escapeCodeTagInterpolation: true,
Expand Down
16 changes: 16 additions & 0 deletions packages/valaxy/node/plugins/markdown/transform/matter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import yaml, { CORE_SCHEMA } from 'js-yaml'
import type matter from 'gray-matter'
import { EXCERPT_SEPARATOR } from '../../../constants'

type GrayMatterOptions = matter.GrayMatterOption<string, GrayMatterOptions>

export const matterOptions: GrayMatterOptions = {
excerpt_separator: EXCERPT_SEPARATOR,
engines: {
yaml: {
// Use the CORE_SCHEMA with more basic support to manually handle time (#409)
parse: (str: string) => yaml.load(str, { schema: CORE_SCHEMA }) as object,
stringify: (data: any) => yaml.dump(data),
},
},
}
8 changes: 1 addition & 7 deletions packages/valaxy/node/plugins/vueRouter.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import VueRouter from 'unplugin-vue-router/vite'
import fs from 'fs-extra'
import { resolve } from 'pathe'
import { isDate } from '@antfu/utils'
import { convert } from 'html-to-text'
import type { ExcerptType, Page } from 'valaxy/types'
import type { RouteMeta } from 'vue-router'
import MarkdownIt from 'markdown-it'
import matter from 'gray-matter'
import type { ValaxyNode } from '../types'

import { matterOptions } from '../utils/matterOptions'
import { matterOptions } from './markdown/transform/matter'
import { presetStatistics } from './presets/statistics'
import { setupMarkdownPlugins } from './markdown'

Expand Down Expand Up @@ -124,11 +123,6 @@ export async function createRouterPlugin(valaxyApp: ValaxyNode) {
mdFm.updated = fs.statSync(path).ctime
}

if (!isDate(mdFm.date))
mdFm.date = new Date(mdFm.date)
if (!isDate(mdFm.updated))
mdFm.updated = new Date(mdFm.updated!)

if (mdFm.from) {
if (Array.isArray(mdFm.from)) {
mdFm.from.forEach((from) => {
Expand Down
1 change: 1 addition & 0 deletions packages/valaxy/node/utils/date.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs-extra'
import { getGitTimestamp } from './getGitTimestamp'

/**
* get created time of file
* @param file
Expand Down
Loading
Loading