obsohtml 1.9.6 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
- buy_me_a_coffee: meiert
1
+ # buy_me_a_coffee: meiert
2
2
  github: j9t
3
- liberapay: j9t
3
+ # liberapay: j9t
@@ -1,4 +1,4 @@
1
- # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference
1
+ # https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference
2
2
 
3
3
  version: 2
4
4
  updates:
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![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)
4
4
 
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.
6
6
 
7
7
  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).
8
8
 
@@ -20,7 +20,7 @@ npm i obsohtml
20
20
 
21
21
  #### Execution
22
22
 
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.
24
24
 
25
25
  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.
26
26
 
@@ -80,6 +80,8 @@ node bin/obsohtml.js -f ../path/to/folder
80
80
 
81
81
  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.
82
82
 
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
+
83
85
  ## Background
84
86
 
85
87
  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.
package/bin/obsohtml.js CHANGED
@@ -18,35 +18,54 @@ const obsoleteAttributes = [
18
18
  'align', 'background', 'bgcolor', 'border', 'frameborder', 'hspace', 'marginheight', 'marginwidth', 'noshade', 'nowrap', 'scrolling', 'valign', 'vspace'
19
19
  ];
20
20
 
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
+
21
37
  // Default project directory (user’s home directory)
22
38
  const defaultProjectDirectory = os.homedir();
23
39
 
40
+ // Track whether any obsolete HTML was found
41
+ let foundObsolete = false;
42
+
24
43
  // Function to find obsolete elements and attributes in a file
25
- async function findObsolete(filePath) {
44
+ function findObsolete(filePath) {
26
45
  const content = fs.readFileSync(filePath, 'utf8');
27
46
 
28
47
  // 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;
32
51
  const message = styleText('blue', `Found obsolete element ${styleText('bold', `'${element}'`)} in ${filePath}`);
33
52
  console.log(message);
34
53
  }
35
- });
54
+ }
36
55
 
37
56
  // 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;
41
60
  const message = styleText('green', `Found obsolete attribute ${styleText('bold', `'${attribute}'`)} in ${filePath}`);
42
61
  console.log(message);
43
62
  }
44
- });
63
+ }
45
64
  }
46
65
 
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
48
67
  function walkDirectory(directory, verbose) {
49
- const MAX_PATH_LENGTH = 255; // Adjust this value based on your OS limits
68
+ const MAX_PATH_LENGTH = 255;
50
69
  let files;
51
70
 
52
71
  try {
@@ -63,25 +82,31 @@ function walkDirectory(directory, verbose) {
63
82
  }
64
83
  }
65
84
 
66
- files.forEach(file => {
85
+ for (const file of files) {
67
86
  const fullPath = path.join(directory, file);
68
87
 
69
88
  if (fullPath.length > MAX_PATH_LENGTH) {
70
89
  if (verbose) console.warn(`Skipping file or directory with path too long: ${fullPath}`);
71
- return;
90
+ continue;
72
91
  }
73
92
 
74
93
  try {
75
94
  const stats = fs.lstatSync(fullPath);
76
95
  if (stats.isSymbolicLink()) {
77
96
  if (verbose) console.warn(`Skipping symbolic link: ${fullPath}`);
78
- return;
97
+ continue;
79
98
  }
80
99
  if (stats.isDirectory()) {
81
- if (file !== 'node_modules') {
100
+ if (!EXCLUDED_DIRS.has(file)) {
82
101
  walkDirectory(fullPath, verbose);
83
102
  }
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
+ ) {
85
110
  findObsolete(fullPath);
86
111
  }
87
112
  } catch (err) {
@@ -91,12 +116,25 @@ function walkDirectory(directory, verbose) {
91
116
  throw err;
92
117
  }
93
118
  }
94
- });
119
+ }
95
120
  }
96
121
 
97
122
  // 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);
100
138
  }
101
139
 
102
140
  // Define command line options
@@ -9,71 +9,115 @@ import { stripVTControlCharacters } from 'node:util';
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const scriptPath = path.join(__dirname, 'obsohtml.js');
11
11
 
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
+
12
21
  describe('ObsoHTML', () => {
13
22
  const tempDir = path.join(__dirname, 'temp_test_dir');
14
23
  const tempFile = path.join(tempDir, 'test.html');
15
24
  const tempFileWithAttributes = path.join(tempDir, 'test_with_attributes.html');
25
+ const tempFileWithMidTagAttribute = path.join(tempDir, 'test_mid_tag_attribute.html');
16
26
  const tempFileWithMinimizedAttributes = path.join(tempDir, 'test_with_minimized_attributes.html');
17
27
  const tempTwigFile = path.join(tempDir, 'test.twig');
28
+ const tempJsxFile = path.join(tempDir, 'test.jsx');
29
+ const tempTsxFile = path.join(tempDir, 'test.tsx');
18
30
 
19
31
  before(() => {
20
- // Create a temporary directory and files
21
32
  if (!fs.existsSync(tempDir)) {
22
33
  fs.mkdirSync(tempDir);
23
34
  }
24
35
  fs.writeFileSync(tempFile, '<!DOCTYPE html><html><title>Test</title><body><center>Test</center></body></html>');
25
36
  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>');
26
38
  fs.writeFileSync(tempFileWithMinimizedAttributes, '<!DOCTYPE html><html><title>Test</title><hr noshade><table><tr><th class=nowrap></table>');
27
39
  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>;');
28
42
  });
29
43
 
30
44
  after(() => {
31
- // Clean up the temporary directory and files
32
45
  fs.unlinkSync(tempFile);
33
46
  fs.unlinkSync(tempFileWithAttributes);
47
+ fs.unlinkSync(tempFileWithMidTagAttribute);
34
48
  fs.unlinkSync(tempFileWithMinimizedAttributes);
35
49
  fs.unlinkSync(tempTwigFile);
50
+ fs.unlinkSync(tempJsxFile);
51
+ fs.unlinkSync(tempTsxFile);
36
52
  fs.rmdirSync(tempDir);
37
53
  });
38
54
 
39
55
  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'"));
43
58
  });
44
59
 
45
60
  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'"));
49
63
  });
50
64
 
51
65
  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'"));
57
69
  });
58
70
 
59
71
  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'"));
65
75
  });
66
76
 
67
77
  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'"));
72
81
  });
73
82
 
74
83
  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'));
78
122
  });
79
- });
123
+ });
@@ -0,0 +1,16 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+
4
+ export default [
5
+ {
6
+ ignores: ['node_modules/**']
7
+ },
8
+ js.configs.recommended,
9
+ {
10
+ languageOptions: {
11
+ ecmaVersion: 'latest',
12
+ sourceType: 'module',
13
+ globals: globals.node,
14
+ },
15
+ },
16
+ ];
package/package.json CHANGED
@@ -15,16 +15,23 @@
15
15
  "qc",
16
16
  "semantics"
17
17
  ],
18
- "license": "CC-BY-SA-4.0",
18
+ "license": "MIT",
19
19
  "name": "obsohtml",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/j9t/obsohtml.git"
23
23
  },
24
+ "devDependencies": {
25
+ "@eslint/js": "^10.0.0",
26
+ "eslint": "^10.0.0",
27
+ "globals": "^17.0.0"
28
+ },
24
29
  "scripts": {
30
+ "lint": "eslint .",
31
+ "lint:fix": "eslint . --fix",
25
32
  "start": "node ./bin/obsohtml.js",
26
33
  "test": "node --test bin/obsohtml.test.js"
27
34
  },
28
35
  "type": "module",
29
- "version": "1.9.6"
36
+ "version": "1.9.8"
30
37
  }
File without changes