Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new npm script runs a .js file via ts-node. By default, ts-node does not execute plain JavaScript unless it is configured with allowJs, so this script can fail in a clean environment. Either run it with node or rename the file to .ts and keep ts-node consistent with the other tools/*.ts scripts.

Suggested change
"convert-links": "ts-node ./tools/convert-links.js"
"convert-links": "node ./tools/convert-links.js"

Copilot uses AI. Check for mistakes.
},
"dependencies": {
"@types/node": "^20.3.2"
Expand Down
111 changes: 111 additions & 0 deletions tools/convert-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
let fs = require('fs');
let path = require('path');

const specifiedPath = process.argv[2];
const linkRegex = /<a\b(?=[^>]*\btarget=["']_blank["'])[^>]*\bhref=["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi;

function isInsideTableCell(content, offset) {
const before = content.slice(0, offset).toLowerCase();
Comment on lines +1 to +8
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tool uses CommonJS require and untyped function signatures, while the existing scripts in tools/ are TypeScript (tools/updateFolder.ts, tools/rename-uid.ts) and are executed via ts-node. For consistency and easier maintenance, consider converting this script to TypeScript (ES imports + basic types) or, if it stays JavaScript, run it with node and keep tools/ scripts consistently authored/executed.

Copilot uses AI. Check for mistakes.
const after = content.slice(offset).toLowerCase();

return before.lastIndexOf('<td') > before.lastIndexOf('</td>') && after.includes('</td>');
}

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 ]+(?=<a\b)/i);

if (!indentMatch) {
return false;
}

const indent = indentMatch[0];
const commentRegex = new RegExp(`^${escapeForRegex(indent)}(?:<!--\s*tab:\s*index\.html\s*-->|<!--\s*HTML\s*-->)\s*$`, 'i');

Check failure

Code scanning / CodeQL

Useless regular-expression character escape High

The escape sequence '\s' is equivalent to just 's', so the sequence is not a character class when it is used in a
regular expression
.

Check failure

Code scanning / CodeQL

Useless regular-expression character escape High

The escape sequence '\s' is equivalent to just 's', so the sequence is not a character class when it is used in a
regular expression
.

Check failure

Code scanning / CodeQL

Useless regular-expression character escape High

The escape sequence '.' is equivalent to just '.', so the sequence may still represent a meta-character when it is used in a
regular expression
.

Check failure

Code scanning / CodeQL

Useless regular-expression character escape High

The escape sequence '\s' is equivalent to just 's', so the sequence is not a character class when it is used in a
regular expression
.

Check failure

Code scanning / CodeQL

Useless regular-expression character escape High

The escape sequence '\s' is equivalent to just 's', so the sequence is not a character class when it is used in a
regular expression
.

Check failure

Code scanning / CodeQL

Useless regular-expression character escape High

The escape sequence '\s' is equivalent to just 's', so the sequence is not a character class when it is used in a
regular expression
.

Check failure

Code scanning / CodeQL

Useless regular-expression character escape High

The escape sequence '\s' is equivalent to just 's', so the sequence is not a character class when it is used in a
regular expression
.
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] : [];
});
Comment on lines +52 to +71
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script recursively enumerates all files under the provided path and reads them as UTF-8. If the user passes the repo root, it will traverse directories like images/, applications/, and even .git/, which is very slow and can also corrupt non-text files if a match happens to occur. Consider skipping well-known non-doc directories and/or filtering to documentation extensions (for example, only .md/.mdx).

Copilot uses AI. Check for mistakes.
}

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})`;
});
Comment on lines +74 to +82
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there is no extension filtering, the converter will also rewrite HTML sample files under applications/** that contain <a ... target="_blank">...</a> (often with nested tags like <b>), producing invalid Markdown inside .html files. Limit conversion to Markdown files (or make the allowed extensions explicit via CLI args) to avoid breaking sample applications.

Copilot uses AI. Check for mistakes.

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