diff --git a/app/.vitepress/src/components/menu/RecursionMenuItem.vue b/app/.vitepress/src/components/menu/RecursionMenuItem.vue index eccf36ed936783aff3c00c3b1e46cdb89ae0ff90..41409b019335910b072d8675835fdb6806a75506 100644 --- a/app/.vitepress/src/components/menu/RecursionMenuItem.vue +++ b/app/.vitepress/src/components/menu/RecursionMenuItem.vue @@ -57,6 +57,7 @@ onBeforeUnmount(() => { v-if="isArray(node.children) && node.children.length > 0" ref="itemRef" class="recursion-sub-menu" + :id="node.id === parentProps?.modelValue ? 'rec-active-menu-item' : undefined" :value="node.id" :selectable="node.type === 'page'" :title="!isZh ? node.label : ''" @@ -68,7 +69,15 @@ onBeforeUnmount(() => { - + {{ node.label }} {{ node.label }} diff --git a/app/.vitepress/src/layouts/LayoutDoc.vue b/app/.vitepress/src/layouts/LayoutDoc.vue index c1df43629c32ce7f5b4e0f856e2d49d2649fef23..5d082b1d1f3c0bb30d489c50cea97d3bcaa7b066 100644 --- a/app/.vitepress/src/layouts/LayoutDoc.vue +++ b/app/.vitepress/src/layouts/LayoutDoc.vue @@ -27,6 +27,7 @@ import { useViewStore } from '@/stores/view'; import { getNodeHrefSafely, type DocMenuNodeT } from '@/utils/tree'; import { scrollIntoView } from '@/utils/scroll-to'; +import { isElementVisible } from '@/utils/element'; const { hash } = useData(); const { lePad, isPhone, size } = useScreen(); @@ -57,6 +58,14 @@ const updateExpandedKeys = () => { } menuExpandedKeys.value = Array.from(set); + + setTimeout(() => { + const parent = document.querySelector('#menuScrollDom .o-scroller-container'); + const el = document.querySelector('#rec-active-menu-item'); + if (parent && el && !isElementVisible(el, parent, 38)) { + scrollIntoView(el, parent, 100, 200); + } + }, 300); } }; @@ -74,6 +83,10 @@ const onPageChange = () => { // 点击菜单跳转文档 const onClickMenuItem = (item: DocMenuNodeT, newOpener?: boolean) => { + if (item.type !== 'page' && item.type !== 'anchor') { + return; + } + const href = getNodeHrefSafely(item); if (href) { if (newOpener) { @@ -260,7 +273,12 @@ onUnmounted(() => { - + diff --git a/app/.vitepress/src/utils/common.ts b/app/.vitepress/src/utils/common.ts index 5231cb2489f2bd4ec8b53f1b50b3b8e96e94def5..fe880173760b69a541e382d40314724cc71f8c5c 100644 --- a/app/.vitepress/src/utils/common.ts +++ b/app/.vitepress/src/utils/common.ts @@ -110,7 +110,11 @@ export const getGiteeUrl = () => { // 分支文档内容 const [_, lang, __, branch, ...others] = pathname.split('/'); - return pathname.includes('/openstack/') - ? `https://gitee.com/openeuler/openstack-docs/tree/openEuler-${branch}/docs/${lang}/${others.slice(3).join('/')}` - : `https://gitee.com/openeuler/docs/tree/${branch}/docs/${lang}/${others.join('/')}`; + if (pathname.includes('/openstack/')) { + return `https://gitee.com/openeuler/openstack-docs/blob/openEuler-${branch}/docs/${lang}/${others.slice(3).join('/')}`; + } else if (pathname.includes('/tools/ai/')) { + return `https://gitee.com/openeuler/euler-copilot-framework/blob/master/${others.slice(3).join('/')}`; + } else { + return `https://gitee.com/openeuler/docs/blob/${branch}/docs/${lang}/${others.join('/')}`; + } }; \ No newline at end of file diff --git a/app/.vitepress/src/views/docs/TheDocsArticle.vue b/app/.vitepress/src/views/docs/TheDocsArticle.vue index a8f939027032f4b4726bb5d4b36fc8f0473f3e27..dc4f6030b07658405cba97e7dc4e11d7deaef181 100644 --- a/app/.vitepress/src/views/docs/TheDocsArticle.vue +++ b/app/.vitepress/src/views/docs/TheDocsArticle.vue @@ -10,7 +10,7 @@ import { useViewStore } from '@/stores/view'; import { useNodeStore } from '@/stores/node'; const emits = defineEmits<{ - (evt: 'scroll-into-title'): void; + (evt: 'update-menu-expaned'): void; (evt: 'change-anchor', value: string): void; (evt: 'page-change', type: 'prev' | 'next'): void; }>(); @@ -120,8 +120,10 @@ onUnmounted(() => { // -------------------- 点击锚点链接滚动到指定位置 -------------------- const onClickContent = (evt: PointerEvent) => { - if (evt.target && (evt.target as HTMLLinkElement)?.href?.includes('#')) { - emits('scroll-into-title'); + if (evt.target && (evt.target as HTMLLinkElement)?.tagName === 'A') { + setTimeout(() => { + emits('update-menu-expaned'); + }, 200); } }; diff --git a/scripts/gen-toc.js b/scripts/gen-toc.js index 33c644d4cf5b5b8b019a6fdd8f449d46d83b26f5..efbaa27d758608413c681850e96eb233d25e315c 100644 --- a/scripts/gen-toc.js +++ b/scripts/gen-toc.js @@ -3,51 +3,37 @@ import path from 'path'; import matter from 'gray-matter'; import markdownIt from 'markdown-it'; import markdownItAnchor from 'markdown-it-anchor'; -import { slugify } from '@mdit-vue/shared'; import yaml from 'js-yaml'; -import { execSync } from 'child_process'; +import { slugify } from '@mdit-vue/shared'; + +import { getGitUrlInfo } from './utils/git.js'; const __dirname = path.resolve(); // 获取当前文件夹路径 -const processedFiles = new Set(); // 记录已处理过的文件路径 +const tempDocsPath = path.join(__dirname, 'temp-docs'); // 文档克隆临时目录 const recordIds = new Set(); // 已处理过的 id const errors = []; /** * git clone - * @param {string} upstream 远程git仓库地址 - * @param {string} localPath 存储地址 + * @param {object} item item */ -function gitClone(upstream, localPath) { - // 创建临时目录 - const tempDocsPath = path.join(__dirname, 'temp-docs'); - fs.removeSync(tempDocsPath); - fs.ensureDirSync(tempDocsPath); +function parseUpstream(item) { + let result = false; try { // 解析url获取仓库信息 - const url = new URL(upstream); - const [owner, repo, __, branch, ...location] = url.pathname.replace('/', '').split('/'); - - // 执行 clone - const gitUrl = `${url.origin}/${owner}/${repo}.git`; - console.log(`执行:git clone --depth=1 -b ${branch} ${gitUrl}`); - const gitTempPath = path.join(tempDocsPath, repo); - execSync(`git clone --depth=1 -b ${branch} ${gitUrl} ${gitTempPath}`); - - // 复制内容 - const tocTempDir = path.join(gitTempPath, ...location.slice(0, -1)); - fs.removeSync(localPath); - fs.copySync(tocTempDir, localPath); + const { repo, locations } = getGitUrlInfo(item.href.upstream); + item.href = item.href.path ? path.join(item.href.path, '_toc.yaml') : path.join(repo, ...(locations.slice(2))); + result = true; } catch (err) { errors.push({ - type: 'Git Clone Exception (git clone 异常)', - file: upstream, - message: err.message.replace(__dirname, '.').replace(/\\/g, '/'), + type: 'Build Exception (构建异常)', + file: item.href.upstream, + message: `parseUpstream - ${err.message.replace(__dirname, '.').replace(/\\/g, '/')}`, }); } - // 清理临时目录 - fs.removeSync(tempDocsPath); + return result; } /** @@ -184,9 +170,8 @@ async function parseNodeSections(dirname, sections) { child.sections = await parseNodeSections(dirname, child.sections); } - if (typeof child?.href?.upstream === 'string' && typeof child?.href?.path === 'string') { - gitClone(child.href.upstream, path.join(dirname, child.href.path)); - child.href = `${child.href.path}${child.href.path.endsWith('/') ? '_toc.yaml' : '/_toc.yaml'}`; + if (typeof child?.href?.upstream === 'string' && !parseUpstream(child)) { + continue; } // 处理 href @@ -208,6 +193,10 @@ async function parseNodeSections(dirname, sections) { const tocPath = path.resolve(dirname, child.href); const parsedChild = await mergeSections(tocPath); if (parsedChild) { + if (child.upstream) { + parsedChild.upstream = child.upstream; + } + parsedSections.push(parsedChild); } continue; @@ -376,6 +365,10 @@ async function createCommonMenu(lang = 'zh') { * 处理文件 */ async function processMenuFile() { + // 创建临时目录 + fs.removeSync(tempDocsPath); + fs.ensureDirSync(tempDocsPath); + const versions = process.argv.slice(2); if (versions.length === 0) { console.error('请提供分支名称'); @@ -423,6 +416,7 @@ async function processMenuFile() { fs.outputFileSync(outputZhPath, JSON.stringify(menuZh, null, 2)); fs.outputFileSync(outputEnPath, JSON.stringify(menuEn, null, 2)); + fs.removeSync(tempDocsPath); console.log(`构建 menu 结束`); } diff --git a/scripts/merge-upstream.js b/scripts/merge-upstream.js new file mode 100644 index 0000000000000000000000000000000000000000..19fe17e0072500db27ee446bd359ac41fbb1698a --- /dev/null +++ b/scripts/merge-upstream.js @@ -0,0 +1,69 @@ +import fs from 'fs'; +import path from 'path'; + +import { getGitUrlInfo, isGitRepo, checkoutBranch } from './utils/git.js'; +import { copyDirectorySync } from './utils/file.js'; + +const REPO_DIR = path.join(process.cwd(), '../../'); +const BUILD_DIR = path.join(process.cwd(), '../../../build'); +const NEW_VERSONS = ['common', '25.03']; + +const copyRepoFromDiskCache = async (upstream, dir, storagePath) => { + const { repo, branch, locations } = getGitUrlInfo(upstream); + const cachePath = path.join(REPO_DIR, repo); + if (!isGitRepo(cachePath)) { + console.log(`不存在 ${repo} 仓库缓存,跳过~`); + } + + await checkoutBranch(cachePath, branch); + const sourceDir = path.join(cachePath, ...locations.slice(0, -1)); + const destDir = storagePath ? path.join(dir, storagePath) : path.join(dir, repo, ...locations.slice(2, -1)); + copyDirectorySync(sourceDir, destDir); + console.log('复制完成'); +} + +const scanYaml = async (yamlPath, dir) => { + const lines = fs.readFileSync(yamlPath, 'utf-8').split('\n'); + let i = 0; + while (i < lines.length) { + if (lines[i].includes('upstream:')) { + const upstream = lines[i].replace('upstream:', '').trim(); + let storagePath = ''; + + if ((i + 1 < lines.length) && lines[i + 1].includes('path:')) { + storagePath = lines[i + 1].replace('path:', '').trim(); + } + + await copyRepoFromDiskCache(upstream, dir, storagePath); + } + i++; + } +}; + +const mergeUpstream = async (targetPath) => { + for (const item of fs.readdirSync(targetPath)) { + const completePath = path.join(targetPath, item); + if (fs.statSync(completePath).isDirectory()) { + await mergeUpstream(completePath); + } else if (item.endsWith('.yaml')) { + await scanYaml(completePath, targetPath); + } + } +}; + +const merge = async () => { + await mergeUpstream(`${BUILD_DIR}/app/zh/`); + await mergeUpstream(`${BUILD_DIR}/app/en/`); +}; + +const args = process.argv.slice(2); +if (args.length === 0) { + console.error('请提供分支名称'); + process.exit(1); +} else { + if (NEW_VERSONS.includes(args[0])) { + merge(args[0]); + } else { + console.error('非新版本内容,跳过处理~'); + } +} diff --git a/scripts/merge.js b/scripts/merge.js index 9c2f190669a5274e007b0fafb512cdc4462336b6..f3f04a3a1112fd33ea7abf5dc5de349160444135 100644 --- a/scripts/merge.js +++ b/scripts/merge.js @@ -2,6 +2,9 @@ import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +// 定义 repo 路径 +const REPO_DIR = path.join(process.cwd(), '../../'); + // 定义docs仓库路径 const REPO_DOCS_DIR = path.join(process.cwd(), '../../docs'); @@ -135,10 +138,8 @@ const getBranchName = (branch) => { const normalizeContent = async (branch) => { const branchName = getBranchName(branch); - // 检出website-v2分支 - await checkoutBranch(REPO_DOCS_DIR, 'website-v2'); - await pullRemoteBranch(REPO_DOCS_DIR, 'website-v2'); - await copyContentToDir(`${REPO_DOCS_DIR}`, BUILD_DIR); + // 复制website-v2内容到build目录 + await copyContentToDir(path.join(REPO_DIR, 'website-v2'), BUILD_DIR); if (branchName == `common`) { // 如果是公共分支,删掉nginx.conf并将nginx.portal.conf重命名为nginx.conf @@ -206,10 +207,9 @@ const normalizeContent = async (branch) => { const normalizeContentWithHugo = async (branch) => { const branchName = getBranchName(branch); - // 检出website分支 - await checkoutBranch(REPO_DOCS_DIR, 'website'); - await pullRemoteBranch(REPO_DOCS_DIR, 'website'); - await copyContentToDir(`${REPO_DOCS_DIR}`, BUILD_DIR); + + // 复制website内容到build目录 + await copyContentToDir(path.join(REPO_DIR, 'website'), BUILD_DIR); let hugoConf = fs.readFileSync(`${BUILD_DIR}/config.toml`, 'utf8'); diff --git a/scripts/pre-dev.js b/scripts/pre-dev.js index 34c6f8af06937a686ae303726806d766167c55ed..fe3d11f68a5cbe7d6d17f949a0b02f2dfdec0adb 100644 --- a/scripts/pre-dev.js +++ b/scripts/pre-dev.js @@ -2,125 +2,64 @@ import { execSync } from 'child_process'; import fs from 'fs-extra'; import path from 'path'; -const ROOT_DIR = path.join(process.cwd(), '../docs'); -const TEMP_DIR = path.join(ROOT_DIR, '../docs-temp'); +const __dirname = path.join(process.cwd(), '../docs'); +const TEMP_DIR = path.join(__dirname, 'temp-docs'); -const shouldIgnore = (filePath) => { - return filePath.includes('node_modules'); -}; -const copyContent = async (sourceDir, destDir, isOuterCall = true) => { - try { - // 仅在最外层调用时清理目标目录 - if (isOuterCall) { - await fs.remove(destDir); - } - - // 确保目标目录存在 - await fs.ensureDir(destDir); - - const entries = await fs.readdir(sourceDir, { withFileTypes: true }); - const copyPromises = entries.map(async (entry) => { - const sourcePath = path.join(sourceDir, entry.name); - const destPath = path.join(destDir, entry.name); - - if (shouldIgnore(sourcePath)) { - return; - } - - if (entry.isDirectory()) { - await copyContent(sourcePath, destPath, false); - } else { - const readStream = fs.createReadStream(sourcePath); - const writeStream = fs.createWriteStream(destPath); - - await new Promise((resolve, reject) => { - readStream.pipe(writeStream); - readStream.on('error', reject); - writeStream.on('error', reject); - writeStream.on('finish', resolve); - }); - } - }); - - await Promise.all(copyPromises); - - if (isOuterCall) { - console.log(`成功将 ${sourceDir} 拷贝到 ${destDir}`); - } - } catch (error) { - console.error('拷贝目录时出错:', error); +const syncTemp = async (branch) => { + if (fs.existsSync(`${TEMP_DIR}`)) { + await fs.rmSync(TEMP_DIR, { recursive: true, force: true }); } + + await execSync(`git clone -b ${branch} --depth=1 https://gitee.com/openeuler/docs.git ${TEMP_DIR}`, { stdio: 'inherit' }); + console.log(`成功克隆分支 ${branch} 到临时目录 ${TEMP_DIR}`); }; + // 获取分支名称 const getBranchName = (branch) => { return branch.replace(/^stable2-/, ''); }; -const syncTemp = async (branchName) => { - await fs.remove(`${TEMP_DIR}`); - await copyContent(ROOT_DIR, TEMP_DIR); - - try { - execSync(`cd ${TEMP_DIR} && git reset --hard`); - execSync(`cd ${TEMP_DIR} && git checkout ${branchName}`); - execSync(`cd ${TEMP_DIR} && git pull`); - console.log(`成功拉取分支代码 ${branchName}`); - } catch (error) { - console.error('拉取分支代码时出错:', error.message); +const cleanDir = (branchName) => { + const dirPath = `${__dirname}/app/zh/docs/${branchName}/`; + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); + console.log(`成功删除 ${dirPath} 文件夹。`); } }; -const normalizeContent = async (branchName) => { - await fs.remove(`${ROOT_DIR}/app/zh/docs/${branchName}/`); - console.log(`成功删除 /app/zh/docs/${branchName}/ 文件夹`); +const normalizeContent = async (branch) => { + const branchName = getBranchName(branch); - await fs.remove(`${ROOT_DIR}/app/en/docs/${branchName}/`); - console.log(`成功删除 /app/en/docs/${branchName}/ 文件夹`); + await cleanDir(branchName); - fs.mkdirSync(`${ROOT_DIR}/app/zh/docs/${branchName}/`, { + fs.mkdirSync(`${__dirname}/app/zh/docs/${branchName}/`, { recursive: true, }); - fs.mkdirSync(`${ROOT_DIR}/app/en/docs/${branchName}/`, { + fs.mkdirSync(`${__dirname}/app/en/docs/${branchName}/`, { recursive: true, }); if (fs.existsSync(`${TEMP_DIR}/docs/zh/`)) { - await copyContent(`${TEMP_DIR}/docs/zh`, `${ROOT_DIR}/app/zh/docs/${branchName}`); - } - if (branchName !== 'common') { - if (fs.existsSync(`${TEMP_DIR}/docs/en/`)) { - await copyContent(`${TEMP_DIR}/docs/en`, `${ROOT_DIR}/app/en/docs/${branchName}`); - } + await fs.copySync(`${TEMP_DIR}/docs/zh`, `${__dirname}/app/zh/docs/${branchName}`); } -}; - -const syncDocs = async (branch, branchAlias) => { - const branchName = getBranchName(branch); - - let branchAliasName; - if (branchAlias) { - branchAliasName = getBranchName(branchAlias); - } else { - branchAliasName = branchName; + if (fs.existsSync(`${TEMP_DIR}/docs/en/`)) { + await fs.copySync(`${TEMP_DIR}/docs/en`, `${__dirname}/app/en/docs/${branchName}`); } +}; - await syncTemp(branchName); - await normalizeContent(branchAliasName); +const syncDocs = async (branch) => { + await syncTemp(branch); + await normalizeContent(branch); - await fs.remove(`${TEMP_DIR}`); + await fs.rmSync(TEMP_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); }; const args = process.argv.slice(2); - if (args.length === 0) { console.error('请提供分支名称'); process.exit(1); } else { - let branchAlias; - if (args[1] == 'as' && args.length === 3) { - branchAlias = args[2]; - } - syncDocs(args[0], args[2]); + syncDocs(args[0]); } diff --git a/scripts/utils/file.js b/scripts/utils/file.js new file mode 100644 index 0000000000000000000000000000000000000000..8f4824fbcb862fb4028fe0cc03ba31b5c41f76c5 --- /dev/null +++ b/scripts/utils/file.js @@ -0,0 +1,19 @@ +import path from 'path'; +import fs from 'fs'; + +export function copyDirectorySync(sourceDir, destDir) { + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + fs.readdirSync(sourceDir, { withFileTypes: true }).forEach((item) => { + const sourcePath = path.join(sourceDir, item.name); + const targetPath = path.join(destDir, item.name); + + if (item.isDirectory()) { + copyDirectorySync(sourcePath, targetPath); + } else { + fs.copyFileSync(sourcePath, targetPath); + } + }); +} diff --git a/scripts/utils/git.js b/scripts/utils/git.js new file mode 100644 index 0000000000000000000000000000000000000000..63913e43f24f827f4e0376889df8e265087f30e5 --- /dev/null +++ b/scripts/utils/git.js @@ -0,0 +1,47 @@ +import fs from 'fs'; +import path from 'path'; +import { spawn } from 'child_process'; + +export function getGitUrlInfo(gitUrl) { + const url = new URL(gitUrl); + const [owner, repo, __, branch, ...locations] = url.pathname.replace('/', '').split('/'); + + return { + url: `${url.origin}/${owner}/${repo}`, + owner, + repo, + branch, + locations, + } +} + +export function isGitRepo(targetPath) { + try { + return fs.statSync(path.join(targetPath, '.git')).isDirectory(); + } catch (err) { + return false; + } +} + +export function checkoutBranch(repoPath, branchName) { + return new Promise((resolve, reject) => { + const child = spawn('git', ['-C', repoPath, 'checkout', branchName]); + child.stdout.on('data', (data) => { + console.log(data.toString()); + }); + child.stderr.on('data', (data) => { + console.error(data.toString()); + }); + child.on('close', (code) => { + if (code === 0) { + console.log(`成功在 ${repoPath} 检出 ${branchName} 分支。`); + resolve(); + } else { + reject(new Error(`在 ${repoPath} 检出 ${branchName} 分支时出现错误`)); + } + }); + child.on('error', (error) => { + reject(error); + }); + }); +}; \ No newline at end of file