diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 45489cd9..0809e3e3 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -178,6 +178,37 @@ importers: specifier: ~5.2.2 version: 5.2.2 + ../../playground/plugin-to-wordpress: + dependencies: + highlight.js: + specifier: ~11.9.0 + version: 11.9.0 + markdown-it: + specifier: ~14.1.0 + version: 14.1.0 + wpapi: + specifier: ~1.2.2 + version: 1.2.2 + devDependencies: + '@elogx-test/elog': + specifier: workspace:* + version: link:../../packages/elog + '@types/markdown-it': + specifier: ~14.0.0 + version: 14.0.0 + '@types/node': + specifier: ~18.15.3 + version: 18.15.13 + '@types/wpapi': + specifier: ~1.1.4 + version: 1.1.4 + tsup: + specifier: ^6.7.0 + version: 6.7.0(typescript@5.2.2) + typescript: + specifier: ~5.2.2 + version: 5.2.2 + ../../tests/test-elog: dependencies: '@elogx-test/elog': @@ -821,6 +852,10 @@ packages: resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} dev: false + /@types/wpapi@1.1.4: + resolution: {integrity: sha512-huqjb3PQ7cJsYlPXxUPDAKSptQ0Ko2gLUZKK7ra9PUU45el0djACDnTJB56nsCPplRZ0C5KJ3xKE9MxvGr1+yQ==} + dev: true + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false @@ -1126,6 +1161,10 @@ packages: engines: {node: '>= 6'} dev: true + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -1135,6 +1174,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: false + /copy-to@2.0.1: resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} dev: false @@ -1620,6 +1663,15 @@ packages: signal-exit: 4.1.0 dev: true + /form-data@2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -1629,6 +1681,11 @@ packages: mime-types: 2.1.35 dev: false + /formidable@1.2.6: + resolution: {integrity: sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==} + deprecated: 'Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau' + dev: false + /formstream@1.3.1: resolution: {integrity: sha512-FkW++ub+VbE5dpwukJVDizNWhSgp8FhmhI65pF7BZSVStBqe6Wgxe2Z9/Vhsn7l7nXCPwP+G1cyYlX8VwWOf0g==} dependencies: @@ -1870,6 +1927,11 @@ packages: space-separated-tokens: 2.0.2 dev: false + /highlight.js@11.9.0: + resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} + engines: {node: '>=12.0.0'} + dev: false + /html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -2152,6 +2214,10 @@ packages: type-check: 0.4.0 dev: true + /li@1.3.0: + resolution: {integrity: sha512-z34TU6GlMram52Tss5mt1m//ifRIpKH5Dqm7yUVOdHI+BQCs9qGPHFaCUTIzsWX7edN30aa2WrPwR7IO10FHaw==} + dev: false + /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -2246,6 +2312,11 @@ packages: engines: {node: '>= 8'} dev: true + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + /micromark-util-character@2.1.0: resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} dependencies: @@ -2470,6 +2541,12 @@ packages: callsites: 3.1.0 dev: true + /parse-link-header@1.0.1: + resolution: {integrity: sha512-Z0gpfHmwCIKDr5rRzjypL+p93aHVWO7e+0rFcUl9E3sC67njjs+xHFenuboSXZGlvYtmQqRzRaE3iFpTUnLmFQ==} + dependencies: + xtend: 4.0.2 + dev: false + /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -2850,6 +2927,24 @@ packages: ts-interface-checker: 0.1.13 dev: true + /superagent@4.1.0: + resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} + engines: {node: '>= 6.0'} + deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.4 + form-data: 2.5.1 + formidable: 1.2.6 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.12.0 + readable-stream: 3.6.2 + transitivePeerDependencies: + - supports-color + dev: false + /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3216,6 +3311,17 @@ packages: semver: 5.7.2 dev: false + /wpapi@1.2.2: + resolution: {integrity: sha512-lkgi8Gjav3SArrCkNpG61ZnmCyamXKB+SjaR8tAoHhSZbJRTeabIlsdqUUAN3JGbVY3ht8p+EGdpCFIaanI5+w==} + dependencies: + li: 1.3.0 + parse-link-header: 1.0.1 + qs: 6.12.0 + superagent: 4.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -3267,6 +3373,11 @@ packages: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: false + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} diff --git a/packages/elog/tsconfig.json b/packages/elog/tsconfig.json index cbfc4e6a..176d5153 100644 --- a/packages/elog/tsconfig.json +++ b/packages/elog/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "ESNext", diff --git a/playground/plugin-from-yuque/package.json b/playground/plugin-from-yuque/package.json index 79d4b276..f137ccf9 100644 --- a/playground/plugin-from-yuque/package.json +++ b/playground/plugin-from-yuque/package.json @@ -16,7 +16,7 @@ "author": "", "license": "MIT", "peerDependencies": { - "@elogx-test/elog": "^1.0.2" + "@elogx-test/elog": "^1.0.0" }, "peerDependenciesMeta": { "@elogx-test/elog": { diff --git a/playground/plugin-from-yuque/tsconfig.json b/playground/plugin-from-yuque/tsconfig.json index cbfc4e6a..176d5153 100644 --- a/playground/plugin-from-yuque/tsconfig.json +++ b/playground/plugin-from-yuque/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "ESNext", diff --git a/playground/plugin-image-local/package.json b/playground/plugin-image-local/package.json index 1a27b73f..3c900597 100644 --- a/playground/plugin-image-local/package.json +++ b/playground/plugin-image-local/package.json @@ -14,7 +14,7 @@ "build": "tsup" }, "peerDependencies": { - "@elogx-test/elog": "^1.0.2" + "@elogx-test/elog": "^1.0.0" }, "peerDependenciesMeta": { "@elogx-test/elog": { diff --git a/playground/plugin-image-local/tsconfig.json b/playground/plugin-image-local/tsconfig.json index cbfc4e6a..176d5153 100644 --- a/playground/plugin-image-local/tsconfig.json +++ b/playground/plugin-image-local/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "ESNext", diff --git a/playground/plugin-to-halo/package.json b/playground/plugin-to-halo/package.json index 5af9b434..3777fc37 100644 --- a/playground/plugin-to-halo/package.json +++ b/playground/plugin-to-halo/package.json @@ -14,7 +14,7 @@ "build": "tsup" }, "peerDependencies": { - "@elogx-test/elog": "^1.0.2" + "@elogx-test/elog": "^1.0.0" }, "peerDependenciesMeta": { "@elogx-test/elog": { diff --git a/playground/plugin-to-halo/src/HaloDeploy.ts b/playground/plugin-to-halo/src/HaloDeploy.ts index 1cf3b0d7..9c208130 100644 --- a/playground/plugin-to-halo/src/HaloDeploy.ts +++ b/playground/plugin-to-halo/src/HaloDeploy.ts @@ -4,27 +4,23 @@ import HaloApi from './HaloApi'; import { slugify } from 'transliteration'; import { delay, getIds, getNoRepValues, htmlAdapter } from './utils'; import type { PostRequest } from '@halo-dev/api-client'; +import Context from './Context'; -export default class { +export default class extends Context { private readonly config: HaloConfig; - private readonly ctx: PluginContext; private readonly api: HaloApi; constructor(config: HaloConfig, ctx: PluginContext) { + super(ctx); this.config = config; - this.ctx = ctx; this.api = new HaloApi(config, ctx); } - /** - * 本地部署 - * @param docs - */ async deploy(docs: DocDetail[]) { if (docs.length === 0) { this.ctx.error('没有可部署的文档'); } - const docDetailList = JSON.parse(JSON.stringify(docs)); + const docDetailList = JSON.parse(JSON.stringify(docs)) as DocDetail[]; this.ctx.success('正在部署到 Halo...'); // 获取文章列表 @@ -126,7 +122,7 @@ export default class { } } for (let doc of docDetailList) { - if (this.config.needUploadImage) { + if (this.config.enableUploadImage) { // 收集文档图片 const urlList = this.ctx.imageUtil.getUrlListFromContent(doc.body); // 封面图 @@ -189,6 +185,7 @@ export default class { } } // markdown转 Html + const mdBody = doc.body; doc.body = htmlAdapter(doc); // 上传文档 @@ -216,7 +213,7 @@ export default class { apiVersion: 'content.halo.run/v1alpha1', kind: 'Post', metadata: { - name: doc.doc_id, + name: doc.id, }, }, content: { @@ -226,7 +223,7 @@ export default class { }, }; // 判断文档是否存在 halo - const item = postMap[doc.doc_id]; + const item = postMap[doc.id]; if (item) { params = item; params.content = { @@ -269,13 +266,13 @@ export default class { params.post.spec.categories = categoryIds; } // 覆盖文档内容 - params.content.content = doc.body_html; + params.content.content = doc.body; if (this.config.rowType === 'markdown') { - params.content.raw = doc.body; + params.content.raw = mdBody; params.content.rawType = 'markdown'; } else { params.content.rawType = 'html'; - params.content.raw = doc.body_html; + params.content.raw = doc.body; } // 判断文档是否存在 halo if (!item) { @@ -291,11 +288,11 @@ export default class { try { // 走更新流程 // 更新基本信息 - await this.api.updatePostInfo(doc.doc_id, params.post); + await this.api.updatePostInfo(doc.id, params.post); // 手动阻塞 500ms await delay(); // 更新内容信息 - await this.api.updatePostContent(doc.doc_id, params.content); + await this.api.updatePostContent(doc.id, params.content); this.ctx.info('更新文档', doc.properties.title); } catch (e: any) { this.ctx.warn(`更新 ${doc.properties.title} 文档失败: ${e.message}`); @@ -309,10 +306,10 @@ export default class { (typeof publish === 'string' && publish === 'true') || (typeof publish === 'boolean' && publish) ) { - await this.api.publishPost(doc.doc_id); + await this.api.publishPost(doc.id); this.ctx.info('发布文档', doc.properties.title); } else { - await this.api.unpublishPost(doc.doc_id); + await this.api.unpublishPost(doc.id); this.ctx.info('下架文档', doc.properties.title); } } diff --git a/playground/plugin-to-halo/src/types.ts b/playground/plugin-to-halo/src/types.ts index 33525bab..27b4dc70 100644 --- a/playground/plugin-to-halo/src/types.ts +++ b/playground/plugin-to-halo/src/types.ts @@ -10,6 +10,6 @@ export interface HaloConfig { policyName?: string; /** 组名称 */ groupName?: string; - needUploadImage?: boolean; + enableUploadImage?: boolean; rowType?: string; } diff --git a/playground/plugin-to-halo/tsconfig.json b/playground/plugin-to-halo/tsconfig.json index cbfc4e6a..176d5153 100644 --- a/playground/plugin-to-halo/tsconfig.json +++ b/playground/plugin-to-halo/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "ESNext", diff --git a/playground/plugin-to-local/package.json b/playground/plugin-to-local/package.json index 2283505d..cbac57aa 100644 --- a/playground/plugin-to-local/package.json +++ b/playground/plugin-to-local/package.json @@ -14,7 +14,7 @@ "build": "tsup" }, "peerDependencies": { - "@elogx-test/elog": "^1.0.2" + "@elogx-test/elog": "^1.0.0" }, "peerDependenciesMeta": { "@elogx-test/elog": { diff --git a/playground/plugin-to-local/tsconfig.json b/playground/plugin-to-local/tsconfig.json index cbfc4e6a..176d5153 100644 --- a/playground/plugin-to-local/tsconfig.json +++ b/playground/plugin-to-local/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "ESNext", diff --git a/playground/plugin-to-wordpress/package.json b/playground/plugin-to-wordpress/package.json new file mode 100644 index 00000000..33776b57 --- /dev/null +++ b/playground/plugin-to-wordpress/package.json @@ -0,0 +1,37 @@ +{ + "name": "@elogx-test/plugin-to-wordpress", + "version": "1.0.2", + "description": "", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "build": "tsup" + }, + "peerDependencies": { + "@elogx-test/elog": "^1.0.0" + }, + "peerDependenciesMeta": { + "@elogx-test/elog": { + "optional": true + } + }, + "dependencies": { + "wpapi": "~1.2.2", + "markdown-it": "~14.1.0", + "highlight.js": "~11.9.0" + }, + "devDependencies": { + "tsup": "^6.7.0", + "@elogx-test/elog": "workspace:*", + "typescript": "~5.2.2", + "@types/node": "~18.15.3", + "@types/wpapi": "~1.1.4", + "@types/markdown-it": "~14.0.0" + } +} diff --git a/playground/plugin-to-wordpress/src/Context.ts b/playground/plugin-to-wordpress/src/Context.ts new file mode 100644 index 00000000..740a435a --- /dev/null +++ b/playground/plugin-to-wordpress/src/Context.ts @@ -0,0 +1,8 @@ +import type { PluginContext } from '@elogx-test/elog'; + +export default class { + readonly ctx: PluginContext; + constructor(ctx: PluginContext) { + this.ctx = ctx; + } +} diff --git a/playground/plugin-to-wordpress/src/WordPressApi.ts b/playground/plugin-to-wordpress/src/WordPressApi.ts new file mode 100644 index 00000000..a9be6866 --- /dev/null +++ b/playground/plugin-to-wordpress/src/WordPressApi.ts @@ -0,0 +1,232 @@ +import { + CreateWordPressPost, + UpdateWordPressPost, + WordPressCategory, + WordPressConfig, + WordPressMedia, + WordPressMediaParams, + WordPressPost, + WordPressTag, +} from './types'; +import Context from './Context'; +import type { PluginContext } from '@elogx-test/elog'; +import WPAPI from 'wpapi'; + +export default class WordPressApi extends Context { + config: WordPressConfig; + wpClient: WPAPI; + + constructor(config: WordPressConfig, ctx: PluginContext) { + super(ctx); + this.config = config; + if (!config.endpoint) { + this.ctx.error('缺少WordPress endpoint'); + } + this.config.username = config.username || process.env.WORDPRESS_USERNAME!; + this.config.password = config.password || process.env.WORDPRESS_PASSWORD!; + if (!this.config.username || !this.config.password) { + this.ctx.error('缺少WordPress账号或密码'); + } + this.wpClient = new WPAPI({ + endpoint: config.endpoint, + username: config.username, + password: config.password, + }); + } + + /** + * 获取文章列表 + */ + async getPostList(pageSize = 100, page = 1): Promise { + return this.wpClient.posts().perPage(pageSize).page(page); + } + + /** + * 获取所有文章 + * @param page + * @param allPosts + */ + async getAllPosts(page = 1, allPosts: WordPressPost[] = []): Promise { + return this.wpClient + .posts() + .perPage(100) + .page(page) + .then((posts) => { + // 将当前页面的文章合并到所有文章数组中 + allPosts = allPosts.concat(posts); + + if (posts.length === 100) { + // 继续获取下一页 + return this.getAllPosts(page + 1, allPosts); + } else { + // 已获取到最后一页或没有文章 + return allPosts; + } + }) + .catch((err) => { + if (err.code === 'rest_post_invalid_page_number') { + // 请求页码超过总页数,直接返回所有文章 + return allPosts; + } else { + this.ctx.error(`获取文章列表失败: ${err.message}`); + } + }); + } + + /** + * 创建文章 + */ + async createPost(post: CreateWordPressPost): Promise { + return this.wpClient.posts().create(post); + } + + /** + * 更新文章 + */ + async updatePost(id: number, post: UpdateWordPressPost): Promise { + return this.wpClient.posts().id(id).update(post); + } + + /** + * 删除文章 + */ + async deletePost(id: number): Promise { + return this.wpClient.posts().id(id).delete(); + } + + /** + * 获取标签 + */ + async getTags(): Promise { + return this.wpClient.tags(); + } + + /** + * 获取全部标签 + */ + async getAllTags(page = 1, allTags: WordPressTag[] = []): Promise { + return this.wpClient + .tags() + .perPage(100) + .page(page) + .then((tags) => { + if (tags.length === 0) return allTags; + // 将当前页面的标签合并到所有标签数组中 + allTags = allTags.concat(tags); + + if (tags.length === 100) { + // 继续获取下一页 + return this.getAllTags(page + 1, allTags); + } else { + // 已获取到最后一页或没有标签 + return allTags; + } + }) + .catch((err) => { + this.ctx.error(`获取标签列表失败${err.message}`); + }); + } + + /** + * 新增标签 + */ + async createTag(tag: { name: string }): Promise { + return this.wpClient.tags().create(tag); + } + + /** + * 获取分类 + */ + async getCategories(): Promise { + return this.wpClient.categories(); + } + + /** + * 获取全部分类 + */ + async getAllCategories( + page = 1, + allCategories: WordPressCategory[] = [], + ): Promise { + return this.wpClient + .categories() + .perPage(100) + .page(page) + .then((categories) => { + if (categories.length === 0) return allCategories; + // 将当前页面的分类合并到所有分类数组中 + allCategories = allCategories.concat(categories); + + if (categories.length === 100) { + // 继续获取下一页 + return this.getAllCategories(page + 1, allCategories); + } else { + // 已获取到最后一页或没有分类 + return allCategories; + } + }) + .catch((err) => { + this.ctx.error(`获取分类列表失败: ${err.message}`); + }); + } + + /** + * 新增分类 + * WordPress的分类存在父子关系,但是先不支持 + * @param category + */ + async createCategory(category: { name: string }): Promise { + return this.wpClient.categories().create(category); + } + + /** + * 获取媒体库 + */ + async getMedia(): Promise { + return this.wpClient.media(); + } + + /** + * 获取全部媒体库 + */ + async getAllMedia(page = 1, allMedia: WordPressMedia[] = []): Promise { + return this.wpClient + .media() + .perPage(100) + .page(page) + .then((medias) => { + // 将当前页面的文章合并到所有媒体数组中 + allMedia = allMedia.concat(medias); + + if (medias.length === 100) { + // 继续获取下一页 + return this.getAllMedia(page + 1, allMedia); + } else { + // 已获取到最后一页或没有媒体 + return allMedia; + } + }) + .catch((err) => { + if (err.code === 'rest_post_invalid_page_number') { + // 请求页码超过总页数,直接返回所有媒体 + return allMedia; + } else { + this.ctx.error(`获取图片列表失败: ${err.message}`); + } + }); + } + + /** + * 上传媒体 + */ + async uploadMedia(file: Buffer, filename: string): Promise { + const imageInfo: WordPressMediaParams = { + title: filename, + description: 'upload by elog', + }; + return this.wpClient + .media() + .file(file as any, filename) + .create(imageInfo); + } +} diff --git a/playground/plugin-to-wordpress/src/WordPressDeploy.ts b/playground/plugin-to-wordpress/src/WordPressDeploy.ts new file mode 100644 index 00000000..cc4e543e --- /dev/null +++ b/playground/plugin-to-wordpress/src/WordPressDeploy.ts @@ -0,0 +1,229 @@ +import type { + CreateWordPressPost, + DocMap, + UpdateWordPressPost, + WordPressConfig, + WordPressPost, +} from './types'; +import type { DocDetail, PluginContext } from '@elogx-test/elog'; +import WordPressApi from './WordPressApi'; +import Context from './Context'; +import { getNoRepValues, htmlAdapterWithHighlight, removeEmptyProperties } from './utils'; + +export default class extends Context { + private readonly config: WordPressConfig; + private readonly api: WordPressApi; + + constructor(config: WordPressConfig, ctx: PluginContext) { + super(ctx); + this.config = config; + this.api = new WordPressApi(config, ctx); + } + + async deploy(docs: DocDetail[]) { + if (docs.length === 0) { + this.ctx.error('没有可部署的文档'); + } + const articleList = JSON.parse(JSON.stringify(docs)) as DocDetail[]; + try { + this.ctx.success('正在部署到 WordPress...'); + let tagsKey = 'tags'; + let categoriesKey = 'categories'; + let urlnameKey = 'urlname'; + let coverKey = 'cover'; + let descriptionKey = 'description'; + // 获取keyMap + if (this.config.keyMap && Object.keys(this.config.keyMap)) { + tagsKey = this.config.keyMap.tags || tagsKey; + categoriesKey = this.config.keyMap.categories || categoriesKey; + urlnameKey = this.config.keyMap.urlname || urlnameKey; + coverKey = this.config.keyMap.cover || coverKey; + descriptionKey = this.config.keyMap.description || descriptionKey; + } + // 重新排序articleList,按照层级更新文章 + // 先更新第一级,再更新第二级... + const sortArticleList = articleList.sort((a, b) => { + if (!a.docStructure || !b.docStructure) { + return 0; + } + return a.docStructure.length - b.docStructure.length; + }); + // 获取文章列表 + const postList = await this.api.getAllPosts(); + let postMap: DocMap = {}; + // List转Map + postList.forEach((item) => { + postMap[item.title.rendered] = item; + }); + // 获取wp标签 + const wpTags = await this.api.getAllTags(); + // 获取wp分类 + const wpCategories = await this.api.getAllCategories(); + // 获取wp媒体 + const wpMedias = await this.api.getAllMedia(); + const noRepValues = getNoRepValues(sortArticleList, tagsKey, categoriesKey); + for (const tag of noRepValues.tags) { + const wpTag = wpTags.find((t) => t.name === tag); + if (!wpTag) { + try { + const newTag = await this.api.createTag({ name: tag }); + wpTags.push(newTag); + } catch (e: any) { + this.ctx.warn(`创建 ${tag} 标签失败: ${e.message}`); + } + } + } + for (const category of noRepValues.categories) { + const wpCategory = wpCategories.find((t) => t.name === category); + if (!wpCategory) { + // 如果没有找到,就在wp创建一个 + try { + const newCategory = await this.api.createCategory({ name: category }); + wpCategories.push(newCategory); + } catch (e: any) { + this.ctx.warn(`创建 ${category} 分类失败: ${e.message}`); + } + } + } + + let publishedPostMap: DocMap = {}; + // 根据目录上传到wp上 + for (const articleInfo of sortArticleList) { + // 重复文档跳过同步 + if (publishedPostMap[articleInfo.properties.title]) { + this.ctx.warn('跳过更新', `存在重复文档:${articleInfo.properties.title}`); + continue; + } + // 自定义处理md文档 + articleInfo.body = htmlAdapterWithHighlight(articleInfo); + const post: UpdateWordPressPost | CreateWordPressPost = { + title: articleInfo.properties.title, + content: articleInfo.body, + status: 'publish', + slug: articleInfo.properties[urlnameKey] || articleInfo.properties.title, + excerpt: articleInfo.properties[descriptionKey], + }; + const postTags = articleInfo.properties[tagsKey] as string | string[]; + if (postTags?.length) { + const tags = Array.isArray(postTags) ? postTags : postTags.split(','); + // 从wpTags中找到对应的tagId + post.tags = tags.map((tag) => { + const wpTag = wpTags.find((t) => t.name === tag)!; + return wpTag?.id; + }); + } + const postCategories = articleInfo.properties[categoriesKey] as string | string[]; + if (postCategories?.length) { + const categories = Array.isArray(postCategories) + ? postCategories + : postCategories.split(','); + // 从wpCategories中用reduce找到对应的categoryIds + post.categories = categories.reduce((acc: number[], cur) => { + const wpCategory = wpCategories.find((t) => t.name === cur); + if (wpCategory) { + acc.push(wpCategory.id); + } + return acc; + }, []); + } + // 处理封面图 + if (articleInfo.properties[coverKey]) { + const picUrl = articleInfo.properties[coverKey]; + const url = this.ctx.imageUtil.cleanUrlParam(picUrl); + const uuid = this.ctx.imageUtil.genUniqueIdFromUrl(url); + const fileType = await this.ctx.imageUtil.getFileType(picUrl); + if (fileType) { + const filename = `${uuid}.${fileType.type}`; + // 检查是否已经存在图片 + const cacheMedia = wpMedias.find((item) => item.title?.rendered === filename); + if (cacheMedia) { + this.ctx.info('忽略上传', `图片已存在: ${cacheMedia.guid.rendered}`); + post.featured_media = cacheMedia.id; + } else { + const pic = await this.ctx.imageUtil.getBufferFromUrl(picUrl); + if (!pic) { + continue; + } + // 上传特色图片 + const media = await this.api.uploadMedia(pic, filename); + this.ctx.info('上传成功', media.guid.rendered); + wpMedias.push(media); + post.featured_media = media.id; + // 替换属性中的图片 + articleInfo.properties[coverKey] = media.guid.rendered; + } + } + } + // 处理文档图片 + if (this.config.enableUploadImage) { + // 收集文档图片 + const urlList = this.ctx.imageUtil.getUrlListFromContent(articleInfo.body); + for (const image of urlList) { + // 生成文件名 + const fileName = this.ctx.imageUtil.genUniqueIdFromUrl(image.url, 28); + // 生成文件名后缀 + const fileType = await this.ctx.imageUtil.getFileType(image.url); + if (!fileType) { + this.ctx.warn( + `${articleInfo?.properties?.title} 存在获取图片类型失败,跳过:${image.url}`, + ); + continue; + } + // 完整文件名 + const fullName = `${fileName}.${fileType.type}`; + // 检查是否存在该文件 + const item = wpMedias.find((item) => item.title?.rendered === fullName); + if (!item) { + // 上传 + // 获取 buffer + const buffer = await this.ctx.imageUtil.getBufferFromUrl(image.original); + if (!buffer) { + this.ctx.warn( + '跳过', + `${articleInfo?.properties?.title} 存在获取图片内容失败:${image.url}`, + ); + continue; + } + try { + const attachment = await this.api.uploadMedia(buffer, fullName); + // const imageUrl = await this.api.getAttachmentPermalink(attachment.metadata.name) + this.ctx.info('上传成功', attachment.guid.rendered); + wpMedias.push(attachment); + // 替换文档中的图片路径 + articleInfo.body = articleInfo.body.replace( + image.original, + attachment.guid.rendered, + ); + } catch (e: any) { + this.ctx.warn( + '跳过', + `${articleInfo?.properties?.title} 存在上传图片失败:${image.url}`, + ); + this.ctx.debug(e); + } + } else { + this.ctx.info('忽略上传', `图片已存在: ${item.guid.rendered}`); + // 替换文档中的图片路径 + articleInfo.body = articleInfo.body.replace(image.original, item.guid.rendered); + } + } + } + const cachePage = postMap[articleInfo.properties.title]; + if (cachePage) { + await this.api.updatePost(cachePage.id, removeEmptyProperties(post)); + this.ctx.info('更新成功', articleInfo.properties.title); + } else { + const newPost = await this.api.createPost( + removeEmptyProperties(post) as CreateWordPressPost, + ); + postMap[newPost.title.rendered] = newPost; + this.ctx.info('新增成功', articleInfo.properties.title); + } + publishedPostMap[articleInfo.properties.title] = cachePage; + } + return undefined; + } catch (error: any) { + this.ctx.error(`部署到 WordPress 失败: ${error.message}`); + } + } +} diff --git a/playground/plugin-to-wordpress/src/index.ts b/playground/plugin-to-wordpress/src/index.ts new file mode 100644 index 00000000..6a3ac182 --- /dev/null +++ b/playground/plugin-to-wordpress/src/index.ts @@ -0,0 +1,13 @@ +import type { IPlugin } from '@elogx-test/elog'; +import WordPressDeploy from './WordPressDeploy'; +import { WordPressConfig } from './types'; + +export default function toLocal(options: Partial): IPlugin { + return { + name: 'to-wordpress', + async deploy(docs) { + const haloDeploy = new WordPressDeploy(options as WordPressConfig, this); + await haloDeploy.deploy(docs); + }, + }; +} diff --git a/playground/plugin-to-wordpress/src/types.ts b/playground/plugin-to-wordpress/src/types.ts new file mode 100644 index 00000000..15617123 --- /dev/null +++ b/playground/plugin-to-wordpress/src/types.ts @@ -0,0 +1,219 @@ +/** + * local 配置 + */ +export interface WordPressConfig { + username: string; + password: string; + endpoint: string; + keyMap?: { + tags?: string; + categories?: string; + urlname?: string; + cover?: string; + description?: string; + }; + namespace?: string; + formatExt?: string; + /** 是否需要上传文章中的图片 */ + enableUploadImage?: boolean; +} + +export interface WordPressConfig { + username: string; + password: string; + endpoint: string; + keyMap?: { + tags?: string; + categories?: string; + urlname?: string; + cover?: string; + description?: string; + }; + namespace?: string; + formatExt?: string; + /** 是否需要上传文章中的图片 */ + enableUploadImage?: boolean; +} + +/** + * 文章详情 + */ +export interface WordPressPost { + id: number; + date: string; + date_gmt: string; + guid: { + rendered: string; + }; + modified: string; + modified_gmt: string; + slug: string; + status: string; + type: string; + link: string; + title: { + rendered: string; + }; + content: { + rendered: string; + protected: boolean; + }; + excerpt: { + rendered: string; + protected: boolean; + }; + author: number; + featured_media: number; + comment_status: string; + ping_status: string; + sticky: boolean; + template: string; + format: string; + meta: any[]; + categories: number[]; + tags: any[]; +} + +export interface WordPressBaseParams { + title?: string; + content?: string; + status?: string; + slug?: string; + categories?: number | number[]; + tags?: number | number[]; + /** 特色图片 */ + featured_media?: number; + excerpt?: string; +} + +export interface CreateWordPressPost extends WordPressBaseParams { + title: string; + content: string; +} + +export type UpdateWordPressPost = WordPressBaseParams; + +export interface WordPressCategory { + id: number; + count: number; + description: string; + link: string; + name: string; + slug: string; // 未分类uncategorized + taxonomy: string; // category + parent: number; + meta: any[]; +} + +export interface WordPressTag { + id: number; + count: number; + description: string; + link: string; + name: string; + slug: string; + taxonomy: string; // post_tag +} + +export interface WordPressMedia { + id: number; + date: string; + date_gmt: string; + guid: { + rendered: string; + }; + modified: string; + modified_gmt: string; + slug: string; + status: string; + type: string; + link: string; + title: { + rendered: string; + }; + author: number; + comment_status: string; + ping_status: string; + template: string; + meta: any[]; + description: { + rendered: string; + }; + caption: { + rendered: string; + }; + alt_text: string; + media_type: string; + mime_type: string; + media_details: { + width: number; + height: number; + file: string; + filesize: number; + sizes: { + medium: { + file: string; + width: number; + height: number; + filesize: number; + mime_type: string; + source_url: string; + }; + thumbnail: { + file: string; + width: number; + height: number; + filesize: number; + mime_type: string; + source_url: string; + }; + full: { + file: string; + width: number; + height: number; + mime_type: string; + source_url: string; + }; + }; + image_meta: { + aperture: string; + credit: string; + camera: string; + caption: string; + created_timestamp: string; + copyright: string; + focal_length: string; + iso: string; + shutter_speed: string; + title: string; + orientation: string; + keywords: any[]; + }; + }; + post: number; + source_url: string; +} + +export interface WordPressMediaParams { + /** 标题 */ + title?: string; + /** 替代文本 */ + alt_text?: string; + /** 说明文字 */ + caption?: string; + /** 描述 */ + description?: string; +} + +export interface NoRepValues { + tags: string[]; + categories: string[]; +} + +export interface DocMap { + [key: string]: T; +} + +export interface AnyObject { + [key: string]: any; +} diff --git a/playground/plugin-to-wordpress/src/utils.ts b/playground/plugin-to-wordpress/src/utils.ts new file mode 100644 index 00000000..834e1137 --- /dev/null +++ b/playground/plugin-to-wordpress/src/utils.ts @@ -0,0 +1,69 @@ +import type { DocDetail } from '@elogx-test/elog'; +import MarkdownIt from 'markdown-it'; +import hljs from 'highlight.js'; +import { AnyObject, NoRepValues } from './types'; + +/** + * markdown转html(代码高亮) + * @param post + */ +export function htmlAdapterWithHighlight(post: DocDetail) { + let { body } = post; + return new MarkdownIt({ + html: true, + xhtmlOut: true, + breaks: true, + linkify: true, + typographer: true, + highlight: function (code: string, lang: string) { + const language = hljs.getLanguage(lang) ? lang : 'plaintext'; + return hljs.highlight(code, { language }).value; + }, + }).render(body); +} + +export function getNoRepValues( + posts: DocDetail[], + tagKey: string, + categoryKey: string, +): NoRepValues { + const values = posts.reduce( + (acc: NoRepValues, cur) => { + const tag = cur.properties[tagKey] as string | string[]; + const category = cur.properties[categoryKey] as string | string[]; + if (typeof tag === 'string') { + acc.tags.push(tag); + } else if (Array.isArray(tag)) { + acc.tags = acc.tags.concat(tag); + } + if (typeof category === 'string') { + acc.categories.push(category); + } else if (Array.isArray(category)) { + acc.categories = acc.categories.concat(category); + } + return acc; + }, + { tags: [], categories: [] }, + ); + // 去重 + return { + tags: Array.from(new Set(values.tags)), + categories: Array.from(new Set(values.categories)), + }; +} + +/** + * 删除对象中的空属性 + * @param obj + */ +export const removeEmptyProperties = (obj: AnyObject): AnyObject => { + const filteredObj: AnyObject = {}; + + Object.entries(obj).forEach(([key, value]) => { + if (value !== null && value !== undefined && value !== '' && value.length !== 0) { + filteredObj[key] = value; + } + }); + + return filteredObj; +}; diff --git a/playground/plugin-to-wordpress/tsconfig.json b/playground/plugin-to-wordpress/tsconfig.json new file mode 100644 index 00000000..176d5153 --- /dev/null +++ b/playground/plugin-to-wordpress/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "declaration": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "esModuleInterop": true, + "useUnknownInCatchVariables": false, + "outDir": "./dist", + "rootDir": "./" + } +} diff --git a/playground/plugin-to-wordpress/tsup.config.ts b/playground/plugin-to-wordpress/tsup.config.ts new file mode 100644 index 00000000..fe455ee9 --- /dev/null +++ b/playground/plugin-to-wordpress/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + // 入口文件 或者可以使用 entryPoints 底层是 esbuild + entry: ['src/index.ts'], + + // 打包类型 支持以下几种 'cjs' | 'esm' | 'iife' + format: ['esm'], + platform: 'node', + + // 生成类型文件 xxx.d.ts + dts: true, + + // 代码分割 默认esm模式支持 如果cjs需要代码分割的话就需要配置为 true + splitting: true, + + // sourcemap + sourcemap: true, +}); diff --git a/rush.json b/rush.json index 23f83c50..fd199f75 100644 --- a/rush.json +++ b/rush.json @@ -510,13 +510,13 @@ "projectFolder": "playground/plugin-to-local", "reviewCategory": "playground", "shouldPublish": true + }, + { + "packageName": "@elogx-test/plugin-to-wordpress", + "projectFolder": "playground/plugin-to-wordpress", + "reviewCategory": "playground", + "versionPolicyName": "playground", + "shouldPublish": true } - // { - // "packageName": "@elogx-test/plugin-to-wordpress", - // "projectFolder": "playground/plugin-to-wordpress", - // "reviewCategory": "playground", - // "versionPolicyName": "playground", - // "shouldPublish": true - // } ] } diff --git a/tests/test-elog/tsconfig.json b/tests/test-elog/tsconfig.json index 55f96561..f666cbd6 100644 --- a/tests/test-elog/tsconfig.json +++ b/tests/test-elog/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2022", "module": "ESNext",