nnbb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +1 -0
- package/.env.local +2 -0
- package/.eslintrc.json +15 -0
- package/.github/stale.yml +16 -0
- package/.github/workflows/codeql-analysis.yml +73 -0
- package/.github/workflows/docker-ghcr.yaml +59 -0
- package/.prettierrc +9 -0
- package/.vscode/launch.json +28 -0
- package/.vscode/settings.json +6 -0
- package/Dockerfile +38 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/blog.config.js +454 -0
- package/components/Ackee.js +83 -0
- package/components/AdBlockDetect.js +40 -0
- package/components/AlgoliaSearchModal.js +250 -0
- package/components/Artalk.js +30 -0
- package/components/Busuanzi.js +26 -0
- package/components/ChatBase.js +19 -0
- package/components/Collapse.tsx +134 -0
- package/components/Comment.js +161 -0
- package/components/CommonHead.tsx +101 -0
- package/components/CommonScript.js +125 -0
- package/components/CusdisComponent.js +35 -0
- package/components/CustomContextMenu.js +221 -0
- package/components/DebugPanel.js +134 -0
- package/components/DisableCopy.js +21 -0
- package/components/Draggable.js +167 -0
- package/components/Equation.js +31 -0
- package/components/ExternalPlugins.js +75 -0
- package/components/ExternalScript.js +29 -0
- package/components/FacebookMessenger.js +255 -0
- package/components/FacebookPage.js +34 -0
- package/components/Fireworks.js +210 -0
- package/components/FlipCard.js +56 -0
- package/components/FlutteringRibbon.js +322 -0
- package/components/FullScreenButton.js +48 -0
- package/components/Giscus.js +33 -0
- package/components/Gitalk.js +42 -0
- package/components/GoogleAdsense.js +111 -0
- package/components/Gtag.js +18 -0
- package/components/HeroIcons.tsx +321 -0
- package/components/KatexReact.js +53 -0
- package/components/LazyImage.js +95 -0
- package/components/Live2D.js +52 -0
- package/components/Loading.js +20 -0
- package/components/Mark.js +28 -0
- package/components/NProgress.ts +8 -0
- package/components/Nest.js +124 -0
- package/components/NotionIcon.js +20 -0
- package/components/NotionPage.tsx +206 -0
- package/components/Player.js +54 -0
- package/components/PrismMac.js +236 -0
- package/components/QrCode.js +34 -0
- package/components/Ribbon.js +98 -0
- package/components/Sakura.js +192 -0
- package/components/Select.js +40 -0
- package/components/ShareBar.js +29 -0
- package/components/ShareButtons.js +403 -0
- package/components/SideBarDrawer.js +50 -0
- package/components/StarrySky.js +130 -0
- package/components/Tabs.js +64 -0
- package/components/ThemeSwitch.js +67 -0
- package/components/Twikoo.js +27 -0
- package/components/TwikooCommentCount.js +22 -0
- package/components/TwikooCommentCounter.js +78 -0
- package/components/TwikooRecentComments.js +11 -0
- package/components/Utterances.js +35 -0
- package/components/VConsole.js +76 -0
- package/components/ValineComponent.js +59 -0
- package/components/ValineCount.js +6 -0
- package/components/ValinePanel.js +3 -0
- package/components/Vercel.tsx +54 -0
- package/components/WWAds.js +18 -0
- package/components/WalineComponent.js +83 -0
- package/components/WebMention.js +173 -0
- package/components/Webwhiz.js +17 -0
- package/components/WordCount.js +73 -0
- package/hooks/useToggleClickOutSide.ts +32 -0
- package/hooks/useWindowSize.ts +30 -0
- package/lib/algolia.js +108 -0
- package/lib/busuanzi.js +99 -0
- package/lib/cache/cacheManager.ts +49 -0
- package/lib/cache/localFileCache.ts +56 -0
- package/lib/cache/memoryMache.ts +20 -0
- package/lib/cache/mongoDbCache.ts +70 -0
- package/lib/cache/types.ts +5 -0
- package/lib/font.js +46 -0
- package/lib/global.tsx +129 -0
- package/lib/gtag.js +17 -0
- package/lib/mailchimp.js +49 -0
- package/lib/memorize.js +0 -0
- package/lib/mhchem.js +1696 -0
- package/lib/notion/getAllCategories.ts +51 -0
- package/lib/notion/getAllPageIds.ts +51 -0
- package/lib/notion/getAllPosts.js +68 -0
- package/lib/notion/getAllTags.ts +43 -0
- package/lib/notion/getNotionData.ts +340 -0
- package/lib/notion/getPageInfoOfPostPage.ts +58 -0
- package/lib/notion/getPageProperties.ts +203 -0
- package/lib/notion/getPageTableOfContents.ts +107 -0
- package/lib/notion/getPostBlocks.ts +147 -0
- package/lib/notion/mapImage.ts +130 -0
- package/lib/notion/types.ts +125 -0
- package/lib/notion.js +2 -0
- package/lib/robots.txt.js +25 -0
- package/lib/rss.js +63 -0
- package/lib/sitemap.xml.js +67 -0
- package/lib/utils.js +212 -0
- package/next-env.d.ts +5 -0
- package/next-i18next.config.js +7 -0
- package/next-sitemap.config.js +11 -0
- package/next.config.js +124 -0
- package/package.json +92 -0
- package/pages/404.tsx +40 -0
- package/pages/[prefix]/[slug].tsx +123 -0
- package/pages/[prefix]/index.tsx +223 -0
- package/pages/_app.js +59 -0
- package/pages/_document.js +42 -0
- package/pages/api/subscribe.js +22 -0
- package/pages/archive/index.tsx +79 -0
- package/pages/category/[category]/index.tsx +87 -0
- package/pages/category/[category]/page/[page].tsx +103 -0
- package/pages/category/index.tsx +43 -0
- package/pages/index.tsx +88 -0
- package/pages/page/[page].tsx +93 -0
- package/pages/search/[keyword]/index.tsx +162 -0
- package/pages/search/[keyword]/page/[page].tsx +166 -0
- package/pages/search/index.tsx +69 -0
- package/pages/sitemap.xml.js +70 -0
- package/pages/tag/[tag]/index.tsx +73 -0
- package/pages/tag/[tag]/page/[page].tsx +87 -0
- package/pages/tag/index.tsx +42 -0
- package/postcss.config.js +6 -0
- package/public/ads.txt +1 -0
- package/public/avatar.png +0 -0
- package/public/avatar.svg +11 -0
- package/public/bg_image.jpg +0 -0
- package/public/css/all.min.css +9 -0
- package/public/css/custom.css +8 -0
- package/public/css/img-shadow.css +5 -0
- package/public/css/prism-mac-style.css +58 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/js/cusdis.es.js +107 -0
- package/public/js/custom.js +1 -0
- package/public/locales/en/common.json +44 -0
- package/public/locales/en/menu.json +9 -0
- package/public/locales/en/nav.json +11 -0
- package/public/locales/zh-CN/common.json +44 -0
- package/public/locales/zh-CN/menu.json +9 -0
- package/public/locales/zh-CN/nav.json +9 -0
- package/public/webfonts/fa-brands-400.ttf +0 -0
- package/public/webfonts/fa-brands-400.woff2 +0 -0
- package/public/webfonts/fa-regular-400.ttf +0 -0
- package/public/webfonts/fa-regular-400.woff2 +0 -0
- package/public/webfonts/fa-solid-900.ttf +0 -0
- package/public/webfonts/fa-solid-900.woff2 +0 -0
- package/public/webfonts/fa-v4compatibility.ttf +0 -0
- package/public/webfonts/fa-v4compatibility.woff2 +0 -0
- package/styles/animate.css +503 -0
- package/styles/globals.css +183 -0
- package/styles/notion.css +2064 -0
- package/styles/nprogress.css +84 -0
- package/styles/prism-theme.css +119 -0
- package/styles/utility-patterns.css +79 -0
- package/tailwind.config.js +37 -0
- package/theme/index.ts +6 -0
- package/theme/types/@theme-components.d.ts +29 -0
- package/theme/useLayout.ts +41 -0
- package/theme/utils.ts +108 -0
- package/themes/innocent/package.json +7 -0
- package/themes/innocent/theme.config.js +1 -0
- package/themes/nobelium/components/Announcement.tsx +27 -0
- package/themes/nobelium/components/ArticleFooter.tsx +39 -0
- package/themes/nobelium/components/ArticleInfo.tsx +58 -0
- package/themes/nobelium/components/ArticleLock.tsx +86 -0
- package/themes/nobelium/components/BlogArchiveItem.js +41 -0
- package/themes/nobelium/components/BlogListBar.js +39 -0
- package/themes/nobelium/components/BlogListPage.tsx +67 -0
- package/themes/nobelium/components/BlogListScroll.tsx +96 -0
- package/themes/nobelium/components/BlogPost.tsx +33 -0
- package/themes/nobelium/components/DarkModeButton.tsx +50 -0
- package/themes/nobelium/components/ExampleRecentComments.js +35 -0
- package/themes/nobelium/components/Footer.tsx +35 -0
- package/themes/nobelium/components/JumpToTopButton.tsx +39 -0
- package/themes/nobelium/components/LanguageSwitchButton.tsx +58 -0
- package/themes/nobelium/components/MenuItemCollapse.tsx +92 -0
- package/themes/nobelium/components/MenuItemDrop.tsx +83 -0
- package/themes/nobelium/components/Nav/Nav.module.css +50 -0
- package/themes/nobelium/components/Nav/Nav.tsx +187 -0
- package/themes/nobelium/components/RandomPostButton.tsx +31 -0
- package/themes/nobelium/components/SearchButton.tsx +31 -0
- package/themes/nobelium/components/SearchInput.tsx +94 -0
- package/themes/nobelium/components/SearchNavBar.js +19 -0
- package/themes/nobelium/components/SideBar.js +83 -0
- package/themes/nobelium/components/SvgIcon.js +29 -0
- package/themes/nobelium/components/TagItem.js +13 -0
- package/themes/nobelium/components/Tags.tsx +44 -0
- package/themes/nobelium/components/Title.js +19 -0
- package/themes/nobelium/index.tsx +28 -0
- package/themes/nobelium/layout/LayoutBase.tsx +79 -0
- package/themes/nobelium/pages/Archive.tsx +30 -0
- package/themes/nobelium/pages/Category.tsx +43 -0
- package/themes/nobelium/pages/Home.tsx +22 -0
- package/themes/nobelium/pages/PageNotFound.tsx +15 -0
- package/themes/nobelium/pages/Post.tsx +34 -0
- package/themes/nobelium/pages/PostList.tsx +74 -0
- package/themes/nobelium/pages/Search.tsx +65 -0
- package/themes/nobelium/pages/Tag.tsx +42 -0
- package/themes/nobelium/providers/index.tsx +60 -0
- package/themes/nobelium/stores/index.tsx +42 -0
- package/themes/nobelium/theme.config.ts +17 -0
- package/themes/nobelium/types/index.ts +10 -0
- package/tsconfig.json +29 -0
- package/types/index.ts +1 -0
- package/types/page.ts +102 -0
- package/vercel.json +5 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
import { getTextContent, getDateValue } from 'notion-utils';
|
2
|
+
// import { NotionAPI } from 'notion-client';
|
3
|
+
import BLOG from '@/blog.config';
|
4
|
+
import md5 from 'js-md5';
|
5
|
+
import { mapImgUrl } from './mapImage';
|
6
|
+
import dayjs from 'dayjs';
|
7
|
+
import { PagePropertiesType, PagePropertiesStatus } from './types';
|
8
|
+
|
9
|
+
import type {
|
10
|
+
BlockMap,
|
11
|
+
PageInfo,
|
12
|
+
CollectionPropertySchemaMap,
|
13
|
+
Decoration,
|
14
|
+
} from './types';
|
15
|
+
|
16
|
+
type TempPageInfo = Partial<PageInfo> & { [key: string]: any };
|
17
|
+
const excludeProperties = ['date', 'select', 'multi_select', 'person'];
|
18
|
+
|
19
|
+
export default async function getPageProperties(
|
20
|
+
id: string,
|
21
|
+
blockMap: BlockMap,
|
22
|
+
schemaMap: CollectionPropertySchemaMap,
|
23
|
+
// authToken?: string,
|
24
|
+
): Promise<PageInfo> {
|
25
|
+
const pageInfo: TempPageInfo = { id: id };
|
26
|
+
const block = blockMap[id].value;
|
27
|
+
|
28
|
+
Object.entries<Decoration[]>(block.properties).forEach(
|
29
|
+
async ([key, value]) => {
|
30
|
+
const schema = schemaMap[key];
|
31
|
+
if (schema.type && !excludeProperties.includes(schema.type)) {
|
32
|
+
pageInfo[schema.name] = getTextContent(value);
|
33
|
+
} else {
|
34
|
+
switch (schema.type) {
|
35
|
+
case 'date': {
|
36
|
+
pageInfo[schema.name] = getDateValue(value);
|
37
|
+
break;
|
38
|
+
}
|
39
|
+
case 'select':
|
40
|
+
case 'multi_select': {
|
41
|
+
pageInfo[schema.name] = getTextContent(value).split(',');
|
42
|
+
break;
|
43
|
+
}
|
44
|
+
case 'person': {
|
45
|
+
// TODO 这段没看懂,以后再研究
|
46
|
+
// const rawUsers = value.flat();
|
47
|
+
// const users = [];
|
48
|
+
// const api = new NotionAPI({ authToken });
|
49
|
+
|
50
|
+
// for (let i = 0; i < rawUsers.length; i++) {
|
51
|
+
// if (rawUsers[i][0][1]) {
|
52
|
+
// const userId = rawUsers[i][0];
|
53
|
+
// const res = await api.getUsers(userId);
|
54
|
+
// const resValue =
|
55
|
+
// res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value;
|
56
|
+
// const user = {
|
57
|
+
// id: resValue?.id,
|
58
|
+
// first_name: resValue?.given_name,
|
59
|
+
// last_name: resValue?.family_name,
|
60
|
+
// profile_photo: resValue?.profile_photo,
|
61
|
+
// };
|
62
|
+
// users.push(user);
|
63
|
+
// }
|
64
|
+
// }
|
65
|
+
// pageInfo[schema.name] = users;
|
66
|
+
break;
|
67
|
+
}
|
68
|
+
default:
|
69
|
+
break;
|
70
|
+
}
|
71
|
+
}
|
72
|
+
},
|
73
|
+
);
|
74
|
+
|
75
|
+
// 映射键:用户自定义表头名
|
76
|
+
const fieldNames = BLOG.NOTION_PROPERTY_NAME;
|
77
|
+
if (fieldNames) {
|
78
|
+
for (const [key, value] of Object.entries(fieldNames)) {
|
79
|
+
if (value && pageInfo[value]) pageInfo[key] = pageInfo[value];
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
// type\status\category 是单选下拉框 取数组第一个
|
84
|
+
if (pageInfo.type && pageInfo.type[0]) {
|
85
|
+
pageInfo.type = pageInfo.type[0] as PagePropertiesType;
|
86
|
+
}
|
87
|
+
if (pageInfo.status && pageInfo.status[0]) {
|
88
|
+
pageInfo.status = pageInfo.status[0] as PagePropertiesStatus;
|
89
|
+
}
|
90
|
+
if (pageInfo.category && pageInfo.category[0])
|
91
|
+
pageInfo.category = pageInfo.category[0];
|
92
|
+
|
93
|
+
if (!pageInfo.tags) pageInfo.tags = [];
|
94
|
+
|
95
|
+
// 映射值:用户个性化type和status字段的下拉框选项,在此映射回代码的英文标识
|
96
|
+
mapProperties(pageInfo);
|
97
|
+
|
98
|
+
pageInfo.publishDate = dayjs(
|
99
|
+
pageInfo?.date?.start_date || block.created_time,
|
100
|
+
).valueOf();
|
101
|
+
pageInfo.lastEditedDate = block?.last_edited_time;
|
102
|
+
pageInfo.pageIcon = mapImgUrl(block?.format?.page_icon, block);
|
103
|
+
pageInfo.pageCover = mapImgUrl(block?.format?.page_cover, block);
|
104
|
+
pageInfo.pageCoverThumbnail = mapImgUrl(
|
105
|
+
block?.format?.page_cover,
|
106
|
+
block,
|
107
|
+
'block',
|
108
|
+
);
|
109
|
+
|
110
|
+
// 处理URL
|
111
|
+
if (pageInfo.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
|
112
|
+
pageInfo.slug = BLOG.POST_URL_PREFIX
|
113
|
+
? generateCustomizeUrl(pageInfo)
|
114
|
+
: pageInfo.slug ?? pageInfo.id;
|
115
|
+
} else if (pageInfo.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
|
116
|
+
pageInfo.slug = pageInfo.slug ?? pageInfo.id;
|
117
|
+
} else if (
|
118
|
+
pageInfo.type === BLOG.NOTION_PROPERTY_NAME.type_menu ||
|
119
|
+
pageInfo.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu
|
120
|
+
) {
|
121
|
+
// 菜单路径为空、作为可展开菜单使用
|
122
|
+
pageInfo.to = pageInfo.slug ?? '#';
|
123
|
+
pageInfo.name = pageInfo.title ?? '';
|
124
|
+
}
|
125
|
+
|
126
|
+
// 开启伪静态路径
|
127
|
+
if (BLOG.PSEUDO_STATIC) {
|
128
|
+
if (
|
129
|
+
!pageInfo?.slug?.endsWith('.html') &&
|
130
|
+
!pageInfo?.slug?.startsWith('http')
|
131
|
+
) {
|
132
|
+
pageInfo.slug += '.html';
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
pageInfo.password = pageInfo.password
|
137
|
+
? md5(pageInfo.slug + pageInfo.password)
|
138
|
+
: '';
|
139
|
+
|
140
|
+
return pageInfo as PageInfo;
|
141
|
+
}
|
142
|
+
|
143
|
+
/**
|
144
|
+
* 映射用户自定义表头
|
145
|
+
*/
|
146
|
+
function mapProperties(properties: TempPageInfo) {
|
147
|
+
if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
|
148
|
+
properties.type = PagePropertiesType.Post;
|
149
|
+
}
|
150
|
+
if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
|
151
|
+
properties.type = PagePropertiesType.Page;
|
152
|
+
}
|
153
|
+
if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_notice) {
|
154
|
+
properties.type = PagePropertiesType.Notice;
|
155
|
+
}
|
156
|
+
if (properties.status === BLOG.NOTION_PROPERTY_NAME.status_publish) {
|
157
|
+
properties.status = PagePropertiesStatus.Published;
|
158
|
+
}
|
159
|
+
if (properties.status === BLOG.NOTION_PROPERTY_NAME.status_invisible) {
|
160
|
+
properties.status = PagePropertiesStatus.Invisible;
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* 获取自定义URL
|
166
|
+
* @param {*} postProperties
|
167
|
+
* @returns
|
168
|
+
*/
|
169
|
+
function generateCustomizeUrl(postProperties: TempPageInfo) {
|
170
|
+
let fullPrefix = '';
|
171
|
+
const allSlugPatterns = BLOG.POST_URL_PREFIX.split('/');
|
172
|
+
allSlugPatterns.forEach((pattern, idx) => {
|
173
|
+
if (pattern === '%year%' && postProperties?.publishDay) {
|
174
|
+
const formatPostCreatedDate = new Date(postProperties?.publishDay);
|
175
|
+
fullPrefix += formatPostCreatedDate.getUTCFullYear();
|
176
|
+
} else if (pattern === '%month%' && postProperties?.publishDay) {
|
177
|
+
const formatPostCreatedDate = new Date(postProperties?.publishDay);
|
178
|
+
fullPrefix += String(formatPostCreatedDate.getUTCMonth() + 1).padStart(
|
179
|
+
2,
|
180
|
+
'0',
|
181
|
+
);
|
182
|
+
} else if (pattern === '%day%' && postProperties?.publishDay) {
|
183
|
+
const formatPostCreatedDate = new Date(postProperties?.publishDay);
|
184
|
+
fullPrefix += String(formatPostCreatedDate.getUTCDate()).padStart(2, '0');
|
185
|
+
} else if (pattern === '%slug%') {
|
186
|
+
fullPrefix += postProperties.slug ?? postProperties.id;
|
187
|
+
} else if (!pattern.includes('%')) {
|
188
|
+
fullPrefix += pattern;
|
189
|
+
} else {
|
190
|
+
return;
|
191
|
+
}
|
192
|
+
if (idx !== allSlugPatterns.length - 1) {
|
193
|
+
fullPrefix += '/';
|
194
|
+
}
|
195
|
+
});
|
196
|
+
if (fullPrefix.startsWith('/')) {
|
197
|
+
fullPrefix = fullPrefix.substring(1); // 去掉头部的"/"
|
198
|
+
}
|
199
|
+
if (fullPrefix.endsWith('/')) {
|
200
|
+
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1); // 去掉尾部部的"/"
|
201
|
+
}
|
202
|
+
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`;
|
203
|
+
}
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import { getTextContent } from 'notion-utils';
|
2
|
+
|
3
|
+
import type {
|
4
|
+
TableOfContentsEntry,
|
5
|
+
PageInfo,
|
6
|
+
ExtendedRecordMap,
|
7
|
+
} from './types';
|
8
|
+
|
9
|
+
const indentLevels = {
|
10
|
+
header: 0,
|
11
|
+
sub_header: 1,
|
12
|
+
sub_sub_header: 2,
|
13
|
+
};
|
14
|
+
|
15
|
+
/**
|
16
|
+
* @see https://github.com/NotionX/react-notion-x/blob/master/packages/notion-utils/src/get-page-table-of-contents.ts
|
17
|
+
* Gets the metadata for a table of contents block by parsing the page's
|
18
|
+
* H1, H2, and H3 elements.
|
19
|
+
*/
|
20
|
+
export const getPageTableOfContents = (
|
21
|
+
page: PageInfo,
|
22
|
+
recordMap: ExtendedRecordMap,
|
23
|
+
): TableOfContentsEntry[] => {
|
24
|
+
const contents = page.content ?? [];
|
25
|
+
const toc = getBlockHeader(contents, recordMap);
|
26
|
+
const indentLevelStack = [
|
27
|
+
{
|
28
|
+
actual: -1,
|
29
|
+
effective: -1,
|
30
|
+
},
|
31
|
+
];
|
32
|
+
|
33
|
+
// Adjust indent levels to always change smoothly.
|
34
|
+
// This is a little tricky, but the key is that when increasing indent levels,
|
35
|
+
// they should never jump more than one at a time.
|
36
|
+
for (const tocItem of toc) {
|
37
|
+
const { indentLevel } = tocItem;
|
38
|
+
const actual = indentLevel;
|
39
|
+
|
40
|
+
do {
|
41
|
+
const prevIndent = indentLevelStack[indentLevelStack.length - 1];
|
42
|
+
const { actual: prevActual, effective: prevEffective } = prevIndent;
|
43
|
+
|
44
|
+
if (actual > prevActual) {
|
45
|
+
tocItem.indentLevel = prevEffective + 1;
|
46
|
+
indentLevelStack.push({
|
47
|
+
actual,
|
48
|
+
effective: tocItem.indentLevel,
|
49
|
+
});
|
50
|
+
} else if (actual === prevActual) {
|
51
|
+
tocItem.indentLevel = prevEffective;
|
52
|
+
break;
|
53
|
+
} else {
|
54
|
+
indentLevelStack.pop();
|
55
|
+
}
|
56
|
+
|
57
|
+
// eslint-disable-next-line no-constant-condition
|
58
|
+
} while (true);
|
59
|
+
}
|
60
|
+
|
61
|
+
return toc;
|
62
|
+
};
|
63
|
+
|
64
|
+
/**
|
65
|
+
* 重写获取目录方法
|
66
|
+
*/
|
67
|
+
function getBlockHeader(
|
68
|
+
contents: string[],
|
69
|
+
recordMap: ExtendedRecordMap,
|
70
|
+
toc?: TableOfContentsEntry[],
|
71
|
+
) {
|
72
|
+
if (!toc) {
|
73
|
+
toc = [];
|
74
|
+
}
|
75
|
+
if (!contents) {
|
76
|
+
return toc;
|
77
|
+
}
|
78
|
+
|
79
|
+
for (const blockId of contents) {
|
80
|
+
const block = recordMap.block[blockId]?.value;
|
81
|
+
if (!block) {
|
82
|
+
continue;
|
83
|
+
}
|
84
|
+
const { type } = block;
|
85
|
+
if (
|
86
|
+
type === 'header' ||
|
87
|
+
type === 'sub_header' ||
|
88
|
+
type === 'sub_sub_header'
|
89
|
+
) {
|
90
|
+
const existed = toc.find((e) => e.id === blockId);
|
91
|
+
if (!existed) {
|
92
|
+
toc.push({
|
93
|
+
id: blockId,
|
94
|
+
type,
|
95
|
+
text: getTextContent(block.properties?.title),
|
96
|
+
indentLevel: indentLevels[type],
|
97
|
+
});
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
if (block.content && block.content?.length > 0) {
|
102
|
+
getBlockHeader(block.content, recordMap, toc);
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
return toc;
|
107
|
+
}
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
import { NotionAPI } from 'notion-client';
|
3
|
+
import { getDataFromCache, setDataToCache } from '@/lib/cache/cacheManager';
|
4
|
+
import { delay } from '../utils';
|
5
|
+
import { cloneDeep } from 'lodash';
|
6
|
+
|
7
|
+
import type { ExtendedRecordMap } from './types';
|
8
|
+
|
9
|
+
/**
|
10
|
+
* 获取文章内容
|
11
|
+
* @param {*} id
|
12
|
+
* @param {*} from
|
13
|
+
* @param {*} slice
|
14
|
+
* @returns
|
15
|
+
*/
|
16
|
+
export async function getPostBlocks(
|
17
|
+
id: string,
|
18
|
+
from: string,
|
19
|
+
slice?: number,
|
20
|
+
): Promise<ExtendedRecordMap> {
|
21
|
+
const cacheKey = 'page_block_' + id;
|
22
|
+
|
23
|
+
try {
|
24
|
+
const pageBlockMapFromCache =
|
25
|
+
await getDataFromCache<ExtendedRecordMap>(cacheKey);
|
26
|
+
if (pageBlockMapFromCache) {
|
27
|
+
console.log('[缓存]:', `from:${from}`, cacheKey);
|
28
|
+
return filterPostBlockMap(id, pageBlockMapFromCache, slice);
|
29
|
+
}
|
30
|
+
|
31
|
+
const start = new Date().getTime();
|
32
|
+
const pageBlockMapFromApi = await getPageWithRetry(id, from);
|
33
|
+
const end = new Date().getTime();
|
34
|
+
console.log('[API耗时]', `${end - start}ms`);
|
35
|
+
|
36
|
+
if (pageBlockMapFromApi) {
|
37
|
+
await setDataToCache(cacheKey, pageBlockMapFromApi);
|
38
|
+
return filterPostBlockMap(id, pageBlockMapFromApi, slice);
|
39
|
+
}
|
40
|
+
} catch (e) {
|
41
|
+
console.error('获取文章内容失败', e);
|
42
|
+
}
|
43
|
+
|
44
|
+
throw Error('获取文章内容失败');
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* 调用接口,失败会重试
|
49
|
+
* @param {*} id
|
50
|
+
* @param {*} retryAttempts
|
51
|
+
*/
|
52
|
+
export async function getPageWithRetry(
|
53
|
+
id: string,
|
54
|
+
from: string,
|
55
|
+
retryAttempts = 3,
|
56
|
+
): Promise<ExtendedRecordMap> {
|
57
|
+
if (retryAttempts && retryAttempts > 0) {
|
58
|
+
console.log(
|
59
|
+
'[请求API]',
|
60
|
+
`from:${from}`,
|
61
|
+
`id:${id}`,
|
62
|
+
retryAttempts < 3 ? `剩余重试次数:${retryAttempts}` : '',
|
63
|
+
);
|
64
|
+
try {
|
65
|
+
const authToken = BLOG.NOTION_ACCESS_TOKEN || '';
|
66
|
+
const api = new NotionAPI({
|
67
|
+
authToken,
|
68
|
+
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
69
|
+
});
|
70
|
+
const pageData = await api.getPage(id);
|
71
|
+
console.info('[响应成功]:', `from:${from}`);
|
72
|
+
return pageData;
|
73
|
+
} catch (e) {
|
74
|
+
console.warn('[响应异常]:', e);
|
75
|
+
await delay(1000);
|
76
|
+
const cacheKey = 'page_block_' + id;
|
77
|
+
const pageBlock = await getDataFromCache<ExtendedRecordMap>(cacheKey);
|
78
|
+
if (pageBlock) {
|
79
|
+
console.log('[重试缓存]', `from:${from}`, `id:${id}`);
|
80
|
+
return pageBlock;
|
81
|
+
}
|
82
|
+
return await getPageWithRetry(id, from, retryAttempts - 1);
|
83
|
+
}
|
84
|
+
} else {
|
85
|
+
console.error('[请求失败]:', `from:${from}`, `id:${id}`);
|
86
|
+
throw new Error('请求失败');
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* 获取到的blockMap删除不需要的字段
|
92
|
+
* @param {*} id 页面ID
|
93
|
+
* @param {*} pageBlockMap 页面元素
|
94
|
+
* @param {*} slice 截取数量
|
95
|
+
* @returns
|
96
|
+
*/
|
97
|
+
function filterPostBlockMap(
|
98
|
+
id: string,
|
99
|
+
pageBlockMap: ExtendedRecordMap,
|
100
|
+
slice?: number,
|
101
|
+
) {
|
102
|
+
const clonePageBlock = cloneDeep(pageBlockMap);
|
103
|
+
let count = 0;
|
104
|
+
|
105
|
+
// for (const i in clonePageBlock.block) {
|
106
|
+
for (const [key, block] of Object.entries(clonePageBlock.block)) {
|
107
|
+
// i= key, b = block
|
108
|
+
if (slice && slice > 0 && count > slice) {
|
109
|
+
delete clonePageBlock.block[key];
|
110
|
+
continue;
|
111
|
+
}
|
112
|
+
// 当BlockId等于PageId时移除
|
113
|
+
if (block.value?.id === id) {
|
114
|
+
// 此block含有敏感信息
|
115
|
+
delete block.value?.properties;
|
116
|
+
continue;
|
117
|
+
}
|
118
|
+
|
119
|
+
count++;
|
120
|
+
// 处理 c++、c#、汇编等语言名字映射
|
121
|
+
if (block.value?.type === 'code') {
|
122
|
+
if (block.value?.properties?.language?.[0][0] === 'C++') {
|
123
|
+
block.value.properties.language[0][0] = 'cpp';
|
124
|
+
}
|
125
|
+
if (block.value?.properties?.language?.[0][0] === 'C#') {
|
126
|
+
block.value.properties.language[0][0] = 'csharp';
|
127
|
+
}
|
128
|
+
if (block.value?.properties?.language?.[0][0] === 'Assembly') {
|
129
|
+
block.value.properties.language[0][0] = 'asm6502';
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
// delete b?.role;
|
134
|
+
// delete b?.value?.version;
|
135
|
+
// delete b?.value?.created_by_table;
|
136
|
+
// delete b?.value?.created_by_id;
|
137
|
+
// delete b?.value?.last_edited_by_table;
|
138
|
+
// delete b?.value?.last_edited_by_id;
|
139
|
+
// delete b?.value?.space_id;
|
140
|
+
}
|
141
|
+
|
142
|
+
// 去掉不用的字段
|
143
|
+
// if (id === BLOG.NOTION_PAGE_ID) {
|
144
|
+
// return clonePageBlock;
|
145
|
+
// }
|
146
|
+
return clonePageBlock;
|
147
|
+
}
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import BLOG from '@/blog.config';
|
2
|
+
import type { Block } from 'notion-types';
|
3
|
+
import { PatchedCollection } from './types';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* 图片映射
|
7
|
+
*
|
8
|
+
* @param {*} img 图片地址,可能是相对路径,可能是外链
|
9
|
+
* @param {*} block 数据块,可能是单个内容块,可能是Page
|
10
|
+
* @param {*} type block 单个内容块 ; collection 集合列表
|
11
|
+
* @param {*} from 来自
|
12
|
+
* @returns
|
13
|
+
*/
|
14
|
+
export const mapImgUrl = (
|
15
|
+
img: string,
|
16
|
+
block: Block | PatchedCollection,
|
17
|
+
type = 'block',
|
18
|
+
needCompress = true,
|
19
|
+
) => {
|
20
|
+
if (!img) {
|
21
|
+
return '';
|
22
|
+
}
|
23
|
+
|
24
|
+
let ret = '';
|
25
|
+
// 相对目录,则视为notion的自带图片
|
26
|
+
if (img.startsWith('/')) {
|
27
|
+
ret = BLOG.NOTION_HOST + img;
|
28
|
+
} else {
|
29
|
+
ret = img;
|
30
|
+
}
|
31
|
+
|
32
|
+
// Notion 图床转换为永久地址
|
33
|
+
const hasConverted =
|
34
|
+
ret.indexOf('https://www.notion.so/image') === 0 ||
|
35
|
+
ret.includes('notion.site/images/page-cover/');
|
36
|
+
|
37
|
+
// 需要转化的URL ; 识别aws图床地址,或者bookmark类型的外链图片
|
38
|
+
const needConvert =
|
39
|
+
!hasConverted &&
|
40
|
+
((block as Block).type === 'bookmark' ||
|
41
|
+
ret.includes('secure.notion-static.com') ||
|
42
|
+
ret.includes('prod-files-secure'));
|
43
|
+
|
44
|
+
// 使用Notion图传
|
45
|
+
if (needConvert) {
|
46
|
+
ret =
|
47
|
+
BLOG.NOTION_HOST +
|
48
|
+
'/image/' +
|
49
|
+
encodeURIComponent(ret) +
|
50
|
+
'?table=' +
|
51
|
+
type +
|
52
|
+
'&id=' +
|
53
|
+
block.id;
|
54
|
+
}
|
55
|
+
|
56
|
+
if (!isEmoji(ret) && ret.indexOf('notion.so/images/page-cover') < 0) {
|
57
|
+
// 图片url优化,确保每一篇文章的图片url唯一
|
58
|
+
if (
|
59
|
+
ret &&
|
60
|
+
ret.length > 4 &&
|
61
|
+
!ret.includes('https://www.notion.so/images/')
|
62
|
+
) {
|
63
|
+
// 图片接口拼接唯一识别参数,防止请求的图片被缓,而导致随机结果相同
|
64
|
+
const separator = ret.includes('?') ? '&' : '?';
|
65
|
+
ret = `${ret.trim()}${separator}t=${block.id}`;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
// 统一压缩图片
|
70
|
+
if (needCompress) {
|
71
|
+
const width = block?.format?.block_width;
|
72
|
+
ret = compressImage(ret, width);
|
73
|
+
}
|
74
|
+
|
75
|
+
return ret;
|
76
|
+
};
|
77
|
+
|
78
|
+
function isEmoji(str: string) {
|
79
|
+
const emojiRegex =
|
80
|
+
/[\u{1F300}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1F018}-\u{1F270}\u{238C}\u{2B06}\u{2B07}\u{2B05}\u{27A1}\u{2194}-\u{2199}\u{2194}\u{21A9}\u{21AA}\u{2934}\u{2935}\u{25AA}\u{25AB}\u{25FE}\u{25FD}\u{25FB}\u{25FC}\u{25B6}\u{25C0}\u{1F200}-\u{1F251}]/u;
|
81
|
+
return emojiRegex.test(str);
|
82
|
+
}
|
83
|
+
|
84
|
+
/**
|
85
|
+
* 压缩图片
|
86
|
+
* 1. Notion图床可以通过指定url-query参数来压缩裁剪图片 例如 ?xx=xx&width=400
|
87
|
+
* 2. UnPlash 图片可以通过api q=50 控制压缩质量 width=400 控制图片尺寸
|
88
|
+
* @param {*} image
|
89
|
+
*/
|
90
|
+
export const compressImage = (
|
91
|
+
image: string,
|
92
|
+
width = 400,
|
93
|
+
quality = 50,
|
94
|
+
fmt = 'webp',
|
95
|
+
) => {
|
96
|
+
if (!image) {
|
97
|
+
return '';
|
98
|
+
}
|
99
|
+
if (
|
100
|
+
image.indexOf(BLOG.NOTION_HOST) === 0 &&
|
101
|
+
image.indexOf('amazonaws.com') > 0
|
102
|
+
) {
|
103
|
+
return `${image}&width=${width}`;
|
104
|
+
}
|
105
|
+
// 压缩unsplash图片
|
106
|
+
if (image.indexOf('https://images.unsplash.com/') === 0) {
|
107
|
+
// 将URL解析为一个对象
|
108
|
+
const urlObj = new URL(image);
|
109
|
+
// 获取URL参数
|
110
|
+
const params = new URLSearchParams(urlObj.search);
|
111
|
+
// 将q参数的值替换
|
112
|
+
params.set('q', quality.toString());
|
113
|
+
// 尺寸
|
114
|
+
params.set('width', width.toString());
|
115
|
+
// 格式
|
116
|
+
params.set('fmt', fmt);
|
117
|
+
params.set('fm', fmt);
|
118
|
+
// 生成新的URL
|
119
|
+
urlObj.search = params.toString();
|
120
|
+
return urlObj.toString();
|
121
|
+
}
|
122
|
+
|
123
|
+
// 此处还可以添加您的自定义图传的封面图压缩参数。
|
124
|
+
// .e.g
|
125
|
+
if (image.indexOf('https://your_picture_bed') === 0) {
|
126
|
+
return 'do_somethin_here';
|
127
|
+
}
|
128
|
+
|
129
|
+
return image;
|
130
|
+
};
|
@@ -0,0 +1,125 @@
|
|
1
|
+
import type {
|
2
|
+
BlockMap,
|
3
|
+
Collection,
|
4
|
+
CollectionPropertySchemaMap,
|
5
|
+
CollectionQueryResult,
|
6
|
+
CollectionViewBlock,
|
7
|
+
CollectionViewMap,
|
8
|
+
CollectionViewPageBlock,
|
9
|
+
ExtendedRecordMap,
|
10
|
+
FormattedDate,
|
11
|
+
ID,
|
12
|
+
BlockType,
|
13
|
+
} from 'notion-types';
|
14
|
+
|
15
|
+
export type * from 'notion-types';
|
16
|
+
|
17
|
+
export interface PageInfo {
|
18
|
+
id: string;
|
19
|
+
title: string;
|
20
|
+
type?: PagePropertiesType;
|
21
|
+
status?: PagePropertiesStatus;
|
22
|
+
category?: string;
|
23
|
+
publishDate: number;
|
24
|
+
lastEditedDate: number;
|
25
|
+
pageIcon: string;
|
26
|
+
pageCover: string;
|
27
|
+
pageCoverThumbnail: string;
|
28
|
+
content: string[];
|
29
|
+
blockMap?: ExtendedRecordMap;
|
30
|
+
date: FormattedDate;
|
31
|
+
icon: string;
|
32
|
+
tags: string[];
|
33
|
+
summary: string;
|
34
|
+
slug: string;
|
35
|
+
results?: string[];
|
36
|
+
password?: string;
|
37
|
+
toc?: TableOfContentsEntry[];
|
38
|
+
}
|
39
|
+
|
40
|
+
export enum PagePropertiesType {
|
41
|
+
Post = 'Post',
|
42
|
+
Page = 'Page',
|
43
|
+
Notice = 'Notice',
|
44
|
+
}
|
45
|
+
|
46
|
+
export enum PagePropertiesStatus {
|
47
|
+
Published = 'Published',
|
48
|
+
Invisible = 'Invisible',
|
49
|
+
Draft = 'Draft',
|
50
|
+
}
|
51
|
+
|
52
|
+
export interface User {
|
53
|
+
id: string;
|
54
|
+
first_name: string;
|
55
|
+
last_name: string;
|
56
|
+
profile_photo: string;
|
57
|
+
}
|
58
|
+
|
59
|
+
export interface CustomNav {
|
60
|
+
icon: string;
|
61
|
+
name: string;
|
62
|
+
to: string;
|
63
|
+
target: string;
|
64
|
+
show: boolean;
|
65
|
+
}
|
66
|
+
|
67
|
+
export interface SiteInfo {
|
68
|
+
title: string;
|
69
|
+
description: string;
|
70
|
+
pageCover: string;
|
71
|
+
icon: string;
|
72
|
+
}
|
73
|
+
|
74
|
+
export interface CategoryInfo {
|
75
|
+
id: string;
|
76
|
+
name: string;
|
77
|
+
color: string;
|
78
|
+
count: number;
|
79
|
+
}
|
80
|
+
|
81
|
+
export interface TagInfo {
|
82
|
+
id: string;
|
83
|
+
name: string;
|
84
|
+
color: string;
|
85
|
+
count: number;
|
86
|
+
}
|
87
|
+
|
88
|
+
export interface DataBaseInfo {
|
89
|
+
notice: PageInfo | null;
|
90
|
+
siteInfo: SiteInfo;
|
91
|
+
allPages: PageInfo[];
|
92
|
+
collection: Collection;
|
93
|
+
collectionQuery: {
|
94
|
+
[collectionId: string]: {
|
95
|
+
[collectionViewId: string]: CollectionQueryResult;
|
96
|
+
};
|
97
|
+
};
|
98
|
+
collectionId: string | null;
|
99
|
+
collectionView: CollectionViewMap;
|
100
|
+
viewIds: string[];
|
101
|
+
block: BlockMap;
|
102
|
+
schema: CollectionPropertySchemaMap;
|
103
|
+
tagOptions: TagInfo[];
|
104
|
+
categoryOptions: CategoryInfo[];
|
105
|
+
rawMetadata: CollectionViewBlock | CollectionViewPageBlock;
|
106
|
+
customNav: CustomNav[];
|
107
|
+
postCount: number;
|
108
|
+
publishedPosts: PageInfo[];
|
109
|
+
pageIds: string[];
|
110
|
+
latestPosts: PageInfo[];
|
111
|
+
}
|
112
|
+
|
113
|
+
// property description and cover are not included in the original Collection type, but it definitely return these property.
|
114
|
+
// so use this interface to patch.
|
115
|
+
export interface PatchedCollection extends Collection {
|
116
|
+
description: [[string]];
|
117
|
+
cover: string;
|
118
|
+
}
|
119
|
+
|
120
|
+
export interface TableOfContentsEntry {
|
121
|
+
id: ID;
|
122
|
+
type: BlockType;
|
123
|
+
text: string;
|
124
|
+
indentLevel: number;
|
125
|
+
}
|