diff --git a/package.json b/package.json index c46976b10e..a115f72f67 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "validate-modules-guide": "dx-tools validate-modules-guide --modules-meta-path ./metadata/modules-meta.json --modules-guide-path \"./concepts/Common/Modularity/02 DevExtreme Modules Structure\"", "validate-links": "dx-tools convert-links --docs-root-path ./ --validation true", "rename-topics": "ts-node ./tools/updateFolder.ts", - "rename-uid": "ts-node ./tools/rename-uid.ts ./api-reference uid-map.json" + "rename-uid": "ts-node ./tools/rename-uid.ts ./api-reference uid-map.json", + "convert-links": "ts-node ./tools/convert-links.js" }, "dependencies": { "@types/node": "^20.3.2" diff --git a/tools/convert-links.js b/tools/convert-links.js new file mode 100644 index 0000000000..c3f7fb2496 --- /dev/null +++ b/tools/convert-links.js @@ -0,0 +1,111 @@ +let fs = require('fs'); +let path = require('path'); + +const specifiedPath = process.argv[2]; +const linkRegex = /]*\btarget=["']_blank["'])[^>]*\bhref=["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi; + +function isInsideTableCell(content, offset) { + const before = content.slice(0, offset).toLowerCase(); + const after = content.slice(offset).toLowerCase(); + + return before.lastIndexOf(' before.lastIndexOf('') && after.includes(''); +} + +function escapeForRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function isInsideCodeSnippet(content, offset) { + const lineStart = content.lastIndexOf('\n', offset - 1) + 1; + const lineEndIndex = content.indexOf('\n', offset); + const lineEnd = lineEndIndex === -1 ? content.length : lineEndIndex; + const line = content.slice(lineStart, lineEnd); + const indentMatch = line.match(/^[\t ]+(?=|)\s*$`, 'i'); + const linesAbove = content.slice(0, lineStart).split(/\r?\n/); + + for (let i = linesAbove.length - 1; i >= 0; i -= 1) { + const currentLine = linesAbove[i]; + + if (!currentLine.trim()) { + continue; + } + + if (commentRegex.test(currentLine)) { + return true; + } + + if (!currentLine.startsWith(indent)) { + return false; + } + } + + return false; +} + +function getFilePaths(targetPath) { + const stats = fs.statSync(targetPath); + + if (stats.isFile()) { + return [targetPath]; + } + + if (!stats.isDirectory()) { + return []; + } + + return fs.readdirSync(targetPath, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(targetPath, entry.name); + + if (entry.isDirectory()) { + return getFilePaths(entryPath); + } + + return entry.isFile() ? [entryPath] : []; + }); +} + +function convertFile(filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + const updatedContent = content.replace(linkRegex, (match, href, innerHtml, offset) => { + if (isInsideTableCell(content, offset) || isInsideCodeSnippet(content, offset)) { + return match; + } + + return `[${innerHtml}](${href})`; + }); + + if (updatedContent !== content) { + fs.writeFileSync(filePath, updatedContent, 'utf-8'); + return true; + } + + return false; +} + +if (!specifiedPath) { + console.error('Specify a file or directory path.'); + process.exit(1); +} + +try { + const filePaths = getFilePaths(specifiedPath); + let updatedFilesCount = 0; + + filePaths.forEach((filePath) => { + if (convertFile(filePath)) { + updatedFilesCount += 1; + } + }); + + console.log(`Processed ${filePaths.length} file(s). Updated ${updatedFilesCount} file(s).`); +} catch (error) { + console.error(error.message || error); + process.exit(1); +}