Skip to content

Commit 5ac3309

Browse files
authored
Merge pull request #35 from j9t/fix/issues
Optimize package
2 parents b664405 + c2ad7a9 commit 5ac3309

File tree

4 files changed

+132
-48
lines changed

4 files changed

+132
-48
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![npm version](https://img.shields.io/npm/v/obsohtml.svg)](https://www.npmjs.com/package/obsohtml) [![Build status](https://github.com/j9t/obsohtml/workflows/Tests/badge.svg)](https://github.com/j9t/obsohtml/actions) [![Socket](https://badge.socket.dev/npm/package/obsohtml)](https://socket.dev/npm/package/obsohtml)
44

5-
ObsoHTML is a Node.js script designed to scan HTML, PHP, Nunjucks, Twig, JavaScript, and TypeScript files for obsolete or proprietary HTML attributes and elements (in scripts, it would catch JSX syntax). It helps you identify and update deprecated HTML code to be more sure to use web standards.
5+
ObsoHTML is a Node.js script designed to scan HTML, PHP, Nunjucks, Twig, JavaScript, and TypeScript files for obsolete or proprietary HTML attributes and elements. It helps you identify and update deprecated HTML code to be more sure to use web standards.
66

77
ObsoHTML has inherent limitations and may not find all obsolete attributes and elements. If you run into a problem, please [file an issue](https://github.com/j9t/obsohtml/issues).
88

@@ -20,7 +20,7 @@ npm i obsohtml
2020

2121
#### Execution
2222

23-
The script accepts a folder path as a command line option, which can be specified in both short form (`-f`) and long form (`--folder`). The folder path can be either absolute or relative.
23+
The script accepts a folder or file path as a command line option, which can be specified in both short form (`-f`) and long form (`--folder`). The path can be either absolute or relative.
2424

2525
The script can be run in “verbose” mode by appending `-v` or `--verbose` to the command. This will show information about files and directories that were skipped.
2626

@@ -80,6 +80,8 @@ node bin/obsohtml.js -f ../path/to/folder
8080

8181
The script will output messages to the console indicating any obsolete attributes or elements found in the scanned files, along with the file paths where they were detected.
8282

83+
The script exits with code `1` if any obsolete HTML is found, and `0` if none is found, making it suitable for use in CI pipelines.
84+
8385
## Background
8486

8587
This started as an experiment, in which I used AI to produce this little HTML quality helper, its tests, and its documentation. While it’s pretty straightforward, I’m sure to have missed something. Please [file an issue](https://github.com/j9t/obsohtml/issues) or contact me directly if you spot a problem or have a suggestion.

bin/obsohtml.js

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,54 @@ const obsoleteAttributes = [
1818
'align', 'background', 'bgcolor', 'border', 'frameborder', 'hspace', 'marginheight', 'marginwidth', 'noshade', 'nowrap', 'scrolling', 'valign', 'vspace'
1919
];
2020

21+
// Pre-compile regexes once at startup
22+
const elementRegexes = obsoleteElements.map(element => ({
23+
element,
24+
regex: new RegExp(`<\\s*${element}\\b`, 'i'),
25+
}));
26+
27+
const attributeRegexes = obsoleteAttributes.map(attribute => ({
28+
attribute,
29+
// Matches the attribute preceded by whitespace anywhere in a tag, without
30+
// requiring it to be the last attribute before the closing bracket.
31+
regex: new RegExp(`<[^>]*\\s${attribute}\\b(\\s*=\\s*(?:"[^"]*"|'[^']*'|[^"'\\s>]+))?`, 'i'),
32+
}));
33+
34+
// Directories to skip during traversal
35+
const EXCLUDED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor']);
36+
2137
// Default project directory (user’s home directory)
2238
const defaultProjectDirectory = os.homedir();
2339

40+
// Track whether any obsolete HTML was found
41+
let foundObsolete = false;
42+
2443
// Function to find obsolete elements and attributes in a file
25-
async function findObsolete(filePath) {
44+
function findObsolete(filePath) {
2645
const content = fs.readFileSync(filePath, 'utf8');
2746

2847
// Check for obsolete elements
29-
obsoleteElements.forEach(element => {
30-
const elementRegex = new RegExp(`<\\s*${element}\\b`, 'i');
31-
if (elementRegex.test(content)) {
48+
for (const { element, regex } of elementRegexes) {
49+
if (regex.test(content)) {
50+
foundObsolete = true;
3251
const message = styleText('blue', `Found obsolete element ${styleText('bold', `'${element}'`)} in ${filePath}`);
3352
console.log(message);
3453
}
35-
});
54+
}
3655

3756
// Check for obsolete attributes
38-
obsoleteAttributes.forEach(attribute => {
39-
const attributeRegex = new RegExp(`<[^>]*\\s${attribute}\\b(\\s*=\\s*(?:"[^"]*"|'[^']*'|[^"'\\s>]+))?\\s*(?=/?>)`, 'i');
40-
if (attributeRegex.test(content)) {
57+
for (const { attribute, regex } of attributeRegexes) {
58+
if (regex.test(content)) {
59+
foundObsolete = true;
4160
const message = styleText('green', `Found obsolete attribute ${styleText('bold', `'${attribute}'`)} in ${filePath}`);
4261
console.log(message);
4362
}
44-
});
63+
}
4564
}
4665

47-
// Function to walk through the project directory, excluding node_modules directories
66+
// Function to walk through the project directory, excluding common build/VCS directories
4867
function walkDirectory(directory, verbose) {
49-
const MAX_PATH_LENGTH = 255; // Adjust this value based on your OS limits
68+
const MAX_PATH_LENGTH = 255;
5069
let files;
5170

5271
try {
@@ -63,25 +82,31 @@ function walkDirectory(directory, verbose) {
6382
}
6483
}
6584

66-
files.forEach(file => {
85+
for (const file of files) {
6786
const fullPath = path.join(directory, file);
6887

6988
if (fullPath.length > MAX_PATH_LENGTH) {
7089
if (verbose) console.warn(`Skipping file or directory with path too long: ${fullPath}`);
71-
return;
90+
continue;
7291
}
7392

7493
try {
7594
const stats = fs.lstatSync(fullPath);
7695
if (stats.isSymbolicLink()) {
7796
if (verbose) console.warn(`Skipping symbolic link: ${fullPath}`);
78-
return;
97+
continue;
7998
}
8099
if (stats.isDirectory()) {
81-
if (file !== 'node_modules') {
100+
if (!EXCLUDED_DIRS.has(file)) {
82101
walkDirectory(fullPath, verbose);
83102
}
84-
} else if (fullPath.endsWith('.html') || fullPath.endsWith('.htm') || fullPath.endsWith('.php') || fullPath.endsWith('.njk') || fullPath.endsWith('.twig') || fullPath.endsWith('.js') || fullPath.endsWith('.ts')) {
103+
} else if (
104+
fullPath.endsWith('.html') || fullPath.endsWith('.htm') ||
105+
fullPath.endsWith('.php') ||
106+
fullPath.endsWith('.njk') || fullPath.endsWith('.twig') ||
107+
fullPath.endsWith('.js') || fullPath.endsWith('.jsx') ||
108+
fullPath.endsWith('.ts') || fullPath.endsWith('.tsx')
109+
) {
85110
findObsolete(fullPath);
86111
}
87112
} catch (err) {
@@ -91,12 +116,25 @@ function walkDirectory(directory, verbose) {
91116
throw err;
92117
}
93118
}
94-
});
119+
}
95120
}
96121

97122
// Main function to execute the script
98-
async function main(projectDirectory = defaultProjectDirectory, verbose = false) {
99-
await walkDirectory(projectDirectory, verbose);
123+
function main(projectDirectory = defaultProjectDirectory, verbose = false) {
124+
let stats;
125+
try {
126+
stats = fs.lstatSync(projectDirectory);
127+
} catch (err) {
128+
if (err.code !== 'ENOENT') throw err;
129+
}
130+
131+
if (stats?.isFile()) {
132+
findObsolete(projectDirectory);
133+
} else {
134+
walkDirectory(projectDirectory, verbose);
135+
}
136+
137+
if (foundObsolete) process.exit(1);
100138
}
101139

102140
// Define command line options

bin/obsohtml.test.js

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,71 +9,115 @@ import { stripVTControlCharacters } from 'node:util';
99
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1010
const scriptPath = path.join(__dirname, 'obsohtml.js');
1111

12+
function run(args) {
13+
const result = spawnSync('node', [scriptPath, ...args], { encoding: 'utf-8' });
14+
return {
15+
stdout: stripVTControlCharacters(result.stdout),
16+
stderr: stripVTControlCharacters(result.stderr),
17+
status: result.status,
18+
};
19+
}
20+
1221
describe('ObsoHTML', () => {
1322
const tempDir = path.join(__dirname, 'temp_test_dir');
1423
const tempFile = path.join(tempDir, 'test.html');
1524
const tempFileWithAttributes = path.join(tempDir, 'test_with_attributes.html');
25+
const tempFileWithMidTagAttribute = path.join(tempDir, 'test_mid_tag_attribute.html');
1626
const tempFileWithMinimizedAttributes = path.join(tempDir, 'test_with_minimized_attributes.html');
1727
const tempTwigFile = path.join(tempDir, 'test.twig');
28+
const tempJsxFile = path.join(tempDir, 'test.jsx');
29+
const tempTsxFile = path.join(tempDir, 'test.tsx');
1830

1931
before(() => {
20-
// Create a temporary directory and files
2132
if (!fs.existsSync(tempDir)) {
2233
fs.mkdirSync(tempDir);
2334
}
2435
fs.writeFileSync(tempFile, '<!DOCTYPE html><html><title>Test</title><body><center>Test</center></body></html>');
2536
fs.writeFileSync(tempFileWithAttributes, '<!DOCTYPE html><html><title>Test</title><body><img src=test.jpg alt=Test align=left></body></html>');
37+
fs.writeFileSync(tempFileWithMidTagAttribute, '<!DOCTYPE html><html><title>Test</title><body><img align=left src=test.jpg></body></html>');
2638
fs.writeFileSync(tempFileWithMinimizedAttributes, '<!DOCTYPE html><html><title>Test</title><hr noshade><table><tr><th class=nowrap></table>');
2739
fs.writeFileSync(tempTwigFile, '<!DOCTYPE html><html><title>Test</title><isindex>');
40+
fs.writeFileSync(tempJsxFile, 'export default () => <center>Hello</center>;');
41+
fs.writeFileSync(tempTsxFile, 'export default (): JSX.Element => <marquee>Hello</marquee>;');
2842
});
2943

3044
after(() => {
31-
// Clean up the temporary directory and files
3245
fs.unlinkSync(tempFile);
3346
fs.unlinkSync(tempFileWithAttributes);
47+
fs.unlinkSync(tempFileWithMidTagAttribute);
3448
fs.unlinkSync(tempFileWithMinimizedAttributes);
3549
fs.unlinkSync(tempTwigFile);
50+
fs.unlinkSync(tempJsxFile);
51+
fs.unlinkSync(tempTsxFile);
3652
fs.rmdirSync(tempDir);
3753
});
3854

3955
test('Detect obsolete elements', () => {
40-
const result = spawnSync('node', [scriptPath, '-f', tempDir], { encoding: 'utf-8' });
41-
const output = stripVTControlCharacters(result.stdout);
42-
assert.ok(output.includes("Found obsolete element 'center'"));
56+
const { stdout } = run(['-f', tempDir]);
57+
assert.ok(stdout.includes("Found obsolete element 'center'"));
4358
});
4459

4560
test('Detect obsolete attributes', () => {
46-
const result = spawnSync('node', [scriptPath, '-f', tempDir], { encoding: 'utf-8' });
47-
const output = stripVTControlCharacters(result.stdout);
48-
assert.ok(output.includes("Found obsolete attribute 'align'"));
61+
const { stdout } = run(['-f', tempDir]);
62+
assert.ok(stdout.includes("Found obsolete attribute 'align'"));
4963
});
5064

5165
test('Detect obsolete elements and attributes using absolute path', () => {
52-
const absolutePath = path.resolve(tempDir);
53-
const result = spawnSync('node', [scriptPath, '-f', absolutePath], { encoding: 'utf-8' });
54-
const output = stripVTControlCharacters(result.stdout);
55-
assert.ok(output.includes("Found obsolete element 'center'"));
56-
assert.ok(output.includes("Found obsolete attribute 'align'"));
66+
const { stdout } = run(['-f', path.resolve(tempDir)]);
67+
assert.ok(stdout.includes("Found obsolete element 'center'"));
68+
assert.ok(stdout.includes("Found obsolete attribute 'align'"));
5769
});
5870

5971
test('Detect obsolete elements and attributes using relative path', () => {
60-
const relativePath = path.relative(process.cwd(), tempDir);
61-
const result = spawnSync('node', [scriptPath, '--folder', relativePath], { encoding: 'utf-8' });
62-
const output = stripVTControlCharacters(result.stdout);
63-
assert.ok(output.includes("Found obsolete element 'center'"));
64-
assert.ok(output.includes("Found obsolete attribute 'align'"));
72+
const { stdout } = run(['--folder', path.relative(process.cwd(), tempDir)]);
73+
assert.ok(stdout.includes("Found obsolete element 'center'"));
74+
assert.ok(stdout.includes("Found obsolete attribute 'align'"));
6575
});
6676

6777
test('Detect obsolete minimized attributes', () => {
68-
const result = spawnSync('node', [scriptPath, '-f', tempDir], { encoding: 'utf-8' });
69-
const output = stripVTControlCharacters(result.stdout);
70-
assert.ok(output.includes("Found obsolete attribute 'noshade'"));
71-
assert.ok(!output.includes("Found obsolete attribute 'nowrap'"));
78+
const { stdout } = run(['-f', tempDir]);
79+
assert.ok(stdout.includes("Found obsolete attribute 'noshade'"));
80+
assert.ok(!stdout.includes("Found obsolete attribute 'nowrap'"));
7281
});
7382

7483
test('Detect obsolete elements in Twig file', () => {
75-
const result = spawnSync('node', [scriptPath, '-f', tempDir], { encoding: 'utf-8' });
76-
const output = stripVTControlCharacters(result.stdout);
77-
assert.ok(output.includes("Found obsolete element 'isindex'"));
84+
const { stdout } = run(['-f', tempDir]);
85+
assert.ok(stdout.includes("Found obsolete element 'isindex'"));
86+
});
87+
88+
test('Detect obsolete attribute when it is not the last attribute in a tag', () => {
89+
const { stdout } = run(['-f', tempFileWithMidTagAttribute]);
90+
assert.ok(stdout.includes("Found obsolete attribute 'align'"));
91+
});
92+
93+
test('Detect obsolete elements in JSX file', () => {
94+
const { stdout } = run(['-f', tempJsxFile]);
95+
assert.ok(stdout.includes("Found obsolete element 'center'"));
96+
});
97+
98+
test('Detect obsolete elements in TSX file', () => {
99+
const { stdout } = run(['-f', tempTsxFile]);
100+
assert.ok(stdout.includes("Found obsolete element 'marquee'"));
101+
});
102+
103+
test('Exit with code 1 when obsolete HTML is found', () => {
104+
const { status } = run(['-f', tempDir]);
105+
assert.strictEqual(status, 1);
106+
});
107+
108+
test('Exit with code 0 when no obsolete HTML is found', () => {
109+
const cleanFile = path.join(tempDir, 'clean.html');
110+
fs.writeFileSync(cleanFile, '<!DOCTYPE html><html><title>Clean</title><body><p>No issues here.</p></body></html>');
111+
try {
112+
const { status } = run(['-f', cleanFile]);
113+
assert.strictEqual(status, 0);
114+
} finally {
115+
fs.unlinkSync(cleanFile);
116+
}
117+
});
118+
119+
test('Verbose mode reports skipped non-existent directory', () => {
120+
const { stderr } = run(['-f', path.join(tempDir, 'nonexistent'), '-v']);
121+
assert.ok(stderr.includes('Skipping non-existent directory'));
78122
});
79-
});
123+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"qc",
1616
"semantics"
1717
],
18-
"license": "CC-BY-SA-4.0",
18+
"license": "MIT",
1919
"name": "obsohtml",
2020
"repository": {
2121
"type": "git",

0 commit comments

Comments
 (0)