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);
+}