diff --git a/docs/.vuepress/notes/zh/theme-guide.ts b/docs/.vuepress/notes/zh/theme-guide.ts index 7e2494b91..d01f56126 100644 --- a/docs/.vuepress/notes/zh/theme-guide.ts +++ b/docs/.vuepress/notes/zh/theme-guide.ts @@ -113,6 +113,7 @@ export const themeGuide = defineNoteConfig({ '加密', '文章贡献者', '文章变更历史', + '文章版权所有', '文章水印', '友情链接页', 'seo', diff --git a/docs/notes/theme/config/frontmatter/basic.md b/docs/notes/theme/config/frontmatter/basic.md index 7c6e6821e..878de339a 100644 --- a/docs/notes/theme/config/frontmatter/basic.md +++ b/docs/notes/theme/config/frontmatter/basic.md @@ -199,6 +199,16 @@ permalink: /config/frontmatter/basic/ 当前页面是否显示页面变更历史。 +### copyright + +- 类型: `boolean | CopyrightLicense | CopyrightFrontmatter` +- 默认值: `false` +- 详情: + + 当前文章是否 显示 版权信息。 + + 完整信息请查看 [copyright](../../guide/功能/文章版权所有.md) + ### editLink - 类型: `boolean` diff --git "a/docs/notes/theme/config/\344\270\273\351\242\230\351\205\215\347\275\256.md" "b/docs/notes/theme/config/\344\270\273\351\242\230\351\205\215\347\275\256.md" index e054349ed..358ddb2e8 100644 --- "a/docs/notes/theme/config/\344\270\273\351\242\230\351\205\215\347\275\256.md" +++ "b/docs/notes/theme/config/\344\270\273\351\242\230\351\205\215\347\275\256.md" @@ -838,6 +838,50 @@ interface SidebarItem { - 默认值: `'View All Changelog'` - 详情: 变更记录的按钮文本 +### copyright + +- 类型: `boolean | CopyrightLicense | CopyrightOptions` +- 默认值: `false` +- 详情: 版权配置 + + 详情请参考 [版权所有](../guide/功能/文章版权所有.md) + +### copyrightText + +- 类型: `string` +- 默认值: `'Copyright'` +- 详情: 版权所有的文本 + +### copyrightAuthorText + +- 类型: `string` +- 默认值: `'Copyright Ownership:'` +- 详情: 版权所有者的文本 + +### copyrightCreationOriginalText + +- 类型: `string` +- 默认值: `'This article link:'` +- 详情: 本文链接的文本 + +### copyrightCreationTranslateText + +- 类型: `string` +- 默认值: `'This article translated from:'` +- 详情: 本文翻译的文本 + +### copyrightCreationReprintText + +- 类型: `string` +- 默认值: `'This article reprint from:'` +- 详情: 本文转载的文本 + +### copyrightLicenseText + +- 类型: `string` +- 默认值: `'License under:'` +- 详情: 版权许可证的文本 + ### prevPage - 类型: `boolean` diff --git "a/docs/notes/theme/guide/\345\212\237\350\203\275/\346\226\207\347\253\240\347\211\210\346\235\203\346\211\200\346\234\211.md" "b/docs/notes/theme/guide/\345\212\237\350\203\275/\346\226\207\347\253\240\347\211\210\346\235\203\346\211\200\346\234\211.md" new file mode 100644 index 000000000..3f192bb35 --- /dev/null +++ "b/docs/notes/theme/guide/\345\212\237\350\203\275/\346\226\207\347\253\240\347\211\210\346\235\203\346\211\200\346\234\211.md" @@ -0,0 +1,254 @@ +--- +title: 文章版权所有 +icon: lucide:creative-commons +badge: + type: tip + text: v1.0.0-rc.118 + +createTime: 2024/11/20 10:52:49 +permalink: /guide/features/copyright/ +--- + + + +## 概述 + +主题支持为文章添加 文章 **版权所有** 声明。 + +文章通常来源于 原创、转载、翻译等。针对于不同的来源,添加版权声明信息能够更好地保护知识产权, +以及避免产生版权纠纷。 + +### Creative Commons + +主题默认支持 [Creative Commons](https://creativecommons.org/) 许可协议的版权声明,包括: + + + +
+ +- [CC0 1.0 通用 (CC0)](https://creativecommons.org/publicdomain/zero/1.0/) + +- [署名 4.0 国际 (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/) + +- [署名-相同方式共享 4.0 国际 (CC-BY-SA-4.0)](https://creativecommons.org/licenses/by-sa/4.0/) + +- [署名-非商业性 4.0 国际 (CC-BY-NC-4.0)](https://creativecommons.org/licenses/by-nc/4.0/) + +- [署名-禁止演绎 4.0 国际 (CC-BY-ND-4.0)](https://creativecommons.org/licenses/by-nd/4.0/) + +- [署名-非商业性-相同方式共享 4.0 国际 (CC-BY-NC-SA-4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/) + +- [署名-非商业性-禁止演绎 4.0 国际 (CC-BY-NC-ND-4.0)](https://creativecommons.org/licenses/by-nc-nd/4.0/) + + +
+ +您可以根据需要选择不同的许可协议,或者自定义许可协议。 + +### 版权信息 + +版权信息包括: + +- 版权所有者,版权所有者链接 +- 版权许可证,版权许可证链接 +- 作品原文链接 + +这些信息将显示在文章的底部。 + +::: tip 使用此功能建议同时启用 [贡献者](./文章贡献者.md) 功能。对于原创文章,主题会自动将文章的第一位贡献者作为版权所有者。你也可以在文章 frontmatter 中手动指定版权所有者。 +::: + +## 全局配置 + +您可以通过以下配置为您的站点的所有文章,声明版权许可证为 `CC-BY-4.0`: + +::: code-tabs +@tab .vuepress/config.ts + +```ts +import { defineUserConfig } from 'vuepress' +import { plumeTheme } from 'vuepress-theme-plume' + +export default defineUserConfig({ + theme: plumeTheme({ + copyright: 'CC-BY-4.0' // [!code hl] + }) +}) +``` + +::: + +您可以通过以下配置为您的站点的所有文章 声明自定义的版权许可证: + +::: code-tabs +@tab .vuepress/config.ts + +```ts :no-line-numbers +import { defineUserConfig } from 'vuepress' +import { plumeTheme } from 'vuepress-theme-plume' + +export default defineUserConfig({ + theme: plumeTheme({ + copyright: { // [!code hl:6] + license: { + name: 'MIT', // 许可证名称 + url: 'https://your-license-url' // 许可证地址 + } + } + }) +}) +``` + +::: + +**配置类型:** + +```ts +export type CopyrightLicense = + | 'CC-BY-4.0' + | 'CC-BY-SA-4.0' + | 'CC-BY-NC-4.0' + | 'CC-BY-NC-SA-4.0' + | 'CC-BY-ND-4.0' + | 'CC-BY-NC-ND-4.0' + | 'CC0' + | string + +/** + * - 配置为 `true` 时,默认为 `CC-BY-4.0` + * - 配置为 `false` 时,不显示版权,但可以在文章 frontmatter.copyright 中覆盖配置 + */ +type CopyrightOptions = boolean | string | CopyrightLicense | { + /** + * 许可证 + */ + license: CopyrightLicense | { + name: CopyrightLicense | string + url: string + } +} +``` + +::: warning 全局配置只适用于 原创文章,对于非原创文章,您应该在文章 frontmatter 中配置版权信息。 +::: + +## 文章 frontmatter 配置 + +您可以在文章 frontmatter 中为单个文章配置版权信息,以覆盖全局配置: + +```md +--- +title: 我的文章 +copyright: CC-BY-4.0 +--- +``` + +**配置类型:** + +```ts +/** + * 配置为 `false` 时,不显示版权 + * 配置为 `true` 时,则默认为 全局配置的 copyright + */ +export type CopyrightFrontmatter = boolean | string | CopyrightLicense | { + /** + * 版权许可 + */ + license?: CopyrightLicense | { name: string, url: string } + + /** + * 版权所有者 + * - 原创文章时默认为文章的第一位贡献者 + * - 非原创文章时需要声明版权所有者 + */ + author?: string | { name: string, url?: string } + + /** + * 作品的创作方式, 原创、翻译、转载 + * @default 'original' + */ + creation?: 'original' | 'translate' | 'reprint' + + /** + * 原文地址,非原创作品时需要声明原文地址 + * @default '' + */ + source?: string +} +``` + +## 文章配置示例 + +### 原创文章 + +```md +--- +title: 我的文章 +copyright: CC-BY-4.0 +--- +``` + + + +### 转载文章 + +```md +--- +title: 转载的文章 +copyright: + creation: reprint + license: CC-BY-4.0 + source: https://example.com/origin + author: + name: 转载者 + url: https://example.com/author +--- +``` + + + +### 翻译文章 + +```md +--- +title: 翻译的文章 +copyright: + creation: translate + license: CC-BY-4.0 + source: https://example.com/origin + author: + name: 原文作者 + url: https://example.com/author +--- +``` + + + +### 自定义许可证 + +```md +--- +title: 我的文章 +copyright: + license: + name: MIT + url: https://example.com/mit +--- +``` + + diff --git a/theme/src/client/components/VPCopyright.vue b/theme/src/client/components/VPCopyright.vue new file mode 100644 index 000000000..1f7da15a6 --- /dev/null +++ b/theme/src/client/components/VPCopyright.vue @@ -0,0 +1,92 @@ + + + + + + + diff --git a/theme/src/client/components/VPDoc.vue b/theme/src/client/components/VPDoc.vue index 070b82cd7..a6a3f147b 100644 --- a/theme/src/client/components/VPDoc.vue +++ b/theme/src/client/components/VPDoc.vue @@ -3,6 +3,7 @@ import VPDocAside from '@theme/VPDocAside.vue' import VPDocBreadcrumbs from '@theme/VPDocBreadcrumbs.vue' import VPDocChangelog from '@theme/VPDocChangelog.vue' import VPDocContributor from '@theme/VPDocContributor.vue' +import VPDocCopyright from '@theme/VPDocCopyright.vue' import VPDocFooter from '@theme/VPDocFooter.vue' import VPDocMeta from '@theme/VPDocMeta.vue' import VPEncryptPage from '@theme/VPEncryptPage.vue' @@ -129,6 +130,7 @@ watch( + diff --git a/theme/src/client/components/VPDocBreadcrumbs.vue b/theme/src/client/components/VPDocBreadcrumbs.vue index ea322670f..a20a70891 100644 --- a/theme/src/client/components/VPDocBreadcrumbs.vue +++ b/theme/src/client/components/VPDocBreadcrumbs.vue @@ -101,6 +101,12 @@ function resolveSidebar( transition: border-left var(--vp-t-color); } +@media print { + .vp-breadcrumb { + display: none; + } +} + .vp-breadcrumb ol { display: flex; flex-wrap: wrap; diff --git a/theme/src/client/components/VPDocChangelog.vue b/theme/src/client/components/VPDocChangelog.vue index d840e7cbc..9fdb28b72 100644 --- a/theme/src/client/components/VPDocChangelog.vue +++ b/theme/src/client/components/VPDocChangelog.vue @@ -69,6 +69,12 @@ const hasChangelog = computed(() => display: block; } +@media print { + .vp-doc-changelog { + display: none; + } +} + .vp-doc-changelog .changelog-header > span { display: block; } diff --git a/theme/src/client/components/VPDocCopyright.vue b/theme/src/client/components/VPDocCopyright.vue new file mode 100644 index 000000000..fac9be791 --- /dev/null +++ b/theme/src/client/components/VPDocCopyright.vue @@ -0,0 +1,40 @@ + + + diff --git a/theme/src/client/composables/copyright.ts b/theme/src/client/composables/copyright.ts new file mode 100644 index 000000000..5baec74fa --- /dev/null +++ b/theme/src/client/composables/copyright.ts @@ -0,0 +1,113 @@ +import type { CopyrightFrontmatter, CopyrightLicense, CopyrightOptions, GitContributor, KnownCopyrightLicense } from '../../shared/index.js' +import { computed, type ComputedRef } from 'vue' +import { useRoute, useRouteLocale } from 'vuepress/client' +import { useContributors } from './contributors.js' +import { useData } from './data.js' +import { getPresetLocaleData } from './preset-locales.js' + +const LICENSE_URL: Record = { + 'CC0': { + url: 'https://creativecommons.org/publicdomain/zero/1.0/', + icons: ['zero'], + }, + 'CC-BY-4.0': { + url: 'https://creativecommons.org/licenses/by/4.0/', + icons: ['cc', 'by'], + }, + 'CC-BY-NC-4.0': { + url: 'https://creativecommons.org/licenses/by-nc/4.0/', + icons: ['cc', 'by', 'nc'], + }, + 'CC-BY-NC-SA-4.0': { + url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/', + icons: ['cc', 'by', 'nc', 'sa'], + }, + 'CC-BY-NC-ND-4.0': { + url: 'https://creativecommons.org/licenses/by-nc-nd/4.0/', + icons: ['cc', 'by', 'nc', 'nd'], + }, + 'CC-BY-ND-4.0': { + url: 'https://creativecommons.org/licenses/by-nd/4.0/', + icons: ['cc', 'by', 'nd'], + }, + 'CC-BY-SA-4.0': { + url: 'https://creativecommons.org/licenses/by-sa/4.0/', + icons: ['cc', 'by', 'sa'], + }, +} + +export function useCopyright(copyright: ComputedRef) { + const { theme } = useData<'post'>() + const route = useRoute() + const routeLocale = useRouteLocale() + const { contributors } = useContributors() + + const hasCopyright = computed(() => Boolean(copyright.value)) + + const creation = computed(() => copyright.value.creation || 'original') + + const license = computed(() => resolveLicense(copyright.value.license, routeLocale.value)) + + const author = computed(() => resolveAuthor(copyright.value.author, creation.value, contributors.value)) + + const sourceUrl = computed(() => { + if (creation.value === 'original') + return __VUEPRESS_SSR__ ? route.fullPath : location.href.split('#')[0] + + return copyright.value.source + }) + + const creationText = computed(() => { + const creation = copyright.value.creation + if (creation === 'translate') { + return theme.value.copyrightCreationTranslateText || 'This article is translated from' + } + else if (creation === 'reprint') { + return theme.value.copyrightCreationReprintText || 'This article is reprint from' + } + return theme.value.copyrightCreationOriginalText || 'This article link: ' + }) + + return { license, author, hasCopyright, creation, creationText, sourceUrl } +} + +interface License { + name: string + url?: string + icons?: string[] +} + +function resolveLicense( + license: CopyrightOptions['license'] = 'CC-BY-4.0', + locale: string, +): License { + const result: License = typeof license === 'string' ? { name: license } : { ...license } + const fallback = LICENSE_URL[result.name] + const name = getPresetLocaleData(locale, result.name as CopyrightLicense) + if (name) { + result.name = `${name} (${result.name})` + } + result.url ||= fallback?.url + result.icons = fallback?.icons + return result +} + +function resolveAuthor( + author: CopyrightFrontmatter['author'], + creation: CopyrightFrontmatter['creation'], + contributors: GitContributor[], +): { name: string, url?: string } | undefined { + const contributor = contributors[0] + + const options = typeof author === 'string' ? { name: author } : author + + if (options && !options.url) { + const contributor = contributors.find(c => c.name === options.name) + if (contributor) + options.url = contributor.url + } + if (creation === 'original' && contributor) + return contributor + + return options +} diff --git a/theme/src/client/composables/index.ts b/theme/src/client/composables/index.ts index 90c41a3eb..cdc1fa0f7 100644 --- a/theme/src/client/composables/index.ts +++ b/theme/src/client/composables/index.ts @@ -7,6 +7,7 @@ export * from './blog-post-list.js' export * from './blog-tags.js' export * from './bulletin.js' export * from './contributors.js' +export * from './copyright.js' export * from './dark-mode.js' export * from './data.js' export * from './edit-link.js' diff --git a/theme/src/node/locales/en.ts b/theme/src/node/locales/en.ts index 8366333a9..fdeaacd4a 100644 --- a/theme/src/node/locales/en.ts +++ b/theme/src/node/locales/en.ts @@ -12,11 +12,17 @@ export const enLocale: PlumeThemeLocaleData = { editLinkText: 'Edit this page', contributorsText: 'Contributors', lastUpdatedText: 'Last Updated', - changelogText: 'Changelog', changelogOnText: 'On', changelogButtonText: 'View All Changelog', + copyrightText: 'Copyright', + copyrightAuthorText: 'Copyright Ownership:', + copyrightCreationOriginalText: 'This article link:', + copyrightCreationTranslateText: 'This article is translated from:', + copyrightCreationReprintText: 'This article is reprint from:', + copyrightLicenseText: 'License under:', + encryptButtonText: 'Confirm', encryptPlaceholder: 'Enter password', encryptGlobalText: 'Only password can access this site', @@ -29,12 +35,21 @@ export const enLocale: PlumeThemeLocaleData = { } export const enPresetLocale: PresetLocale = { - home: 'Home', - blog: 'Blog', - tag: 'Tags', - archive: 'Archives', - category: 'Categories', - archiveTotal: '{count} articles', + 'home': 'Home', + 'blog': 'Blog', + 'tag': 'Tags', + 'archive': 'Archives', + 'category': 'Categories', + 'archiveTotal': '{count} articles', + + // ------ copyright license ------ + 'CC0': 'CC0 1.0 Universal', + 'CC-BY-4.0': 'Attribution 4.0 International', + 'CC-BY-NC-4.0': 'Attribution-NonCommercial 4.0 International', + 'CC-BY-NC-SA-4.0': 'Attribution-NonCommercial-ShareAlike 4.0 International', + 'CC-BY-NC-ND-4.0': 'Attribution-NonCommercial-NoDerivatives 4.0 International', + 'CC-BY-ND-4.0': 'Attribution-NoDerivatives 4.0 International', + 'CC-BY-SA-4.0': 'Attribution-ShareAlike 4.0 International', } export const enSearchLocale: Partial = { diff --git a/theme/src/node/locales/zh.ts b/theme/src/node/locales/zh.ts index d18ec88d2..4443937ac 100644 --- a/theme/src/node/locales/zh.ts +++ b/theme/src/node/locales/zh.ts @@ -22,6 +22,13 @@ export const zhLocale: PlumeThemeLocaleData = { changelogOnText: '于', changelogButtonText: '查看全部变更历史', + copyrightText: '版权所有', + copyrightAuthorText: '版权归属于:', + copyrightCreationOriginalText: '本文链接:', + copyrightCreationTranslateText: '本文翻译自:', + copyrightCreationReprintText: '本文转载自:', + copyrightLicenseText: '许可证:', + notFound: { code: '404', title: '页面未找到', @@ -41,12 +48,21 @@ export const zhLocale: PlumeThemeLocaleData = { } export const zhPresetLocale: PresetLocale = { - home: '首页', - blog: '博客', - tag: '标签', - archive: '归档', - category: '分类', - archiveTotal: '{count} 篇', + 'home': '首页', + 'blog': '博客', + 'tag': '标签', + 'archive': '归档', + 'category': '分类', + 'archiveTotal': '{count} 篇', + + // ------ copyright license ------ + 'CC0': 'CC0 1.0 通用', + 'CC-BY-4.0': '署名 4.0 国际', + 'CC-BY-NC-4.0': '署名-非商业性 4.0 国际', + 'CC-BY-NC-SA-4.0': '署名-非商业性-相同方式共享 4.0 国际', + 'CC-BY-NC-ND-4.0': '署名-非商业性-禁止演绎 4.0 国际', + 'CC-BY-ND-4.0': '署名-禁止演绎 4.0 国际', + 'CC-BY-SA-4.0': '署名-相同方式共享 4.0 国际', } export const zhDocsearchLocale: DocSearchLocaleOptions = { diff --git a/theme/src/shared/base.ts b/theme/src/shared/base.ts index b244e25ef..b3033d5fc 100644 --- a/theme/src/shared/base.ts +++ b/theme/src/shared/base.ts @@ -1,3 +1,12 @@ +/** + * A literal type that supports custom further strings but preserves autocompletion in IDEs. + * + * @see [copied from issue](https://github.com/microsoft/TypeScript/issues/29729#issuecomment-471566609) + */ +export type LiteralUnion = + | Union + | (Base & { zz_IGNORE_ME?: never }) + export type ThemeImage = | string | { src: string, alt?: string } @@ -41,7 +50,7 @@ export type SocialLinkIconUnion = export type SocialLinkIcon = SocialLinkIconUnion | { svg: string, name?: string } -export interface PresetLocale { +export interface PresetLocale extends Record { home: string blog: string tag: string @@ -67,3 +76,45 @@ export interface ThemeTransition { */ appearance?: boolean | 'fade' | 'circle-clip' | 'horizontal-clip' | 'vertical-clip' | 'skew-clip' } + +export type KnownCopyrightLicense = + | 'CC-BY-4.0' + | 'CC-BY-SA-4.0' + | 'CC-BY-NC-4.0' + | 'CC-BY-NC-SA-4.0' + | 'CC-BY-ND-4.0' + | 'CC-BY-NC-ND-4.0' + | 'CC0' + +export type CopyrightLicense = LiteralUnion + +export interface CopyrightOptions { + /** + * 版权信息 + * @see https://creativecommons.org/share-your-work/cclicenses/ + * @default 'CC-BY-4.0' + */ + license?: CopyrightLicense | { name: string, url: string } +} + +export interface CopyrightFrontmatter extends CopyrightOptions { + /** + * 作品的作者 + * + * 如果是 原创,则默认为 contributors 中的第一个,否则需要手动指定 + * @default '' + */ + author?: string | { name: string, url?: string } + + /** + * 作品的创作方式 + * @default 'original' + */ + creation?: 'original' | 'translate' | 'reprint' + + /** + * 原文地址,非 原创 作品时需要声明原文地址 + * @default '' + */ + source?: string +} diff --git a/theme/src/shared/frontmatter/post.ts b/theme/src/shared/frontmatter/post.ts index c53c51399..4183c8872 100644 --- a/theme/src/shared/frontmatter/post.ts +++ b/theme/src/shared/frontmatter/post.ts @@ -1,3 +1,4 @@ +import type { CopyrightFrontmatter, CopyrightLicense } from '../base.js' import type { BlogPostCover } from '../blog.js' import type { PlumeThemePageFrontmatter } from './page.js' @@ -38,4 +39,9 @@ export interface PlumeThemePostFrontmatter extends PlumeThemePageFrontmatter { * 是否展示文章摘要,传入 string 时为自定义摘要,此时 `` 无效 */ excerpt?: boolean | string + + /** + * 版权信息 + */ + copyright?: boolean | CopyrightLicense | CopyrightFrontmatter } diff --git a/theme/src/shared/options/locale.ts b/theme/src/shared/options/locale.ts index 7dce11545..ac9205418 100644 --- a/theme/src/shared/options/locale.ts +++ b/theme/src/shared/options/locale.ts @@ -1,5 +1,5 @@ import type { LocaleData } from 'vuepress/core' -import type { SocialLink, SocialLinkIconUnion, ThemeOutline, ThemeTransition } from '../base.js' +import type { CopyrightLicense, CopyrightOptions, SocialLink, SocialLinkIconUnion, ThemeOutline, ThemeTransition } from '../base.js' import type { PlumeThemeBlog } from '../blog.js' import type { NavItem } from '../navbar.js' import type { NotesOptions } from '../notes.js' @@ -115,6 +115,48 @@ export interface PlumeThemeLocaleData extends LocaleData { */ bulletin?: true | BulletinOptions + /** + * 配置版权信息 + * @default false + */ + copyright?: boolean | CopyrightLicense | CopyrightOptions + + /** + * 版权所有的文本 + * @default 'Copyright' + */ + copyrightText?: string + + /** + * 版权归属者的文本 + * @default 'Copyright Ownership:' + */ + copyrightAuthorText?: string + + /** + * 本文链接的文本 + * @default 'This article link:' + */ + copyrightCreationOriginalText?: string + + /** + * 本文翻译链接的文本 + * @default 'This article is translated from:' + */ + copyrightCreationTranslateText?: string + + /** + * 转载链接的文本 + * @default 'This article is reprint from:' + */ + copyrightCreationReprintText?: string + + /** + * 许可证的文本 + * @default 'License under:' + */ + copyrightLicenseText?: string + /** * 是否启用过渡动画效果 *