Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: suggest correct paths based on levensteihn distance #367

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions .changeset/gentle-crabs-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'check-html-links': minor
---

suggest correct paths based on levenshtein distance
2 changes: 1 addition & 1 deletion packages/check-html-links/src/CheckHtmlLinksCli.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class CheckHtmlLinksCli {
)} missing reference targets (used by ${referenceCount} links) while checking ${
files.length
} files:`,
...formatErrors(errors)
...formatErrors(errors, { files })
.split('\n')
.map(line => ` ${line}`),
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
Expand Down
27 changes: 25 additions & 2 deletions packages/check-html-links/src/formatErrors.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import path from 'path';
import chalk from 'chalk';
import levenshtein from './levenshtein.js';

/** @typedef {import('../types/main').Error} Error */

/**
* @param {Error[]} errors
* @param {*} relativeFrom
* @param {{ relativeFrom?: string; files: string[] }} opts
*/
export function formatErrors(errors, relativeFrom = process.cwd()) {
// @ts-expect-error we need empty obj to destructure from
export function formatErrors(errors, { relativeFrom = process.cwd(), files } = {}) {
let output = [];
let number = 0;
for (const error of errors) {
Expand Down Expand Up @@ -41,6 +43,27 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
output.push(` ... ${more} more references to this target`);
}
output.push('');

/**
* Also consider finding the updated path. This can be useful when documentation is restructured
* For instance, the folder name was changed, but the file name was not.
*/
let suggestion;
let lowestScore = -1;
files.forEach(file => {
const filePathToCompare = file.replace(relativeFrom + '/', '');
const score = levenshtein(filePathToCompare, filePath);
if (score && (lowestScore === -1 || score < lowestScore)) {
lowestScore = score;
suggestion = filePathToCompare;
}
});

if (suggestion) {
output.push(
chalk.italic(`Suggestion: did you mean ${chalk.magenta(suggestion)} instead?\n\n`),
);
}
}
return output.join('\n');
}
107 changes: 107 additions & 0 deletions packages/check-html-links/src/levenshtein.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* eslint-disable */
// https://github.com/gustf/js-levenshtein/blob/master/index.js

/**
* @param {number} d0
* @param {number} d1
* @param {number} d2
* @param {any} bx
* @param {any} ay
*/
function _min(d0, d1, d2, bx, ay) {
return d0 < d1 || d2 < d1 ? (d0 > d2 ? d2 + 1 : d0 + 1) : bx === ay ? d1 : d1 + 1;
}

/**
* @param {string} a
* @param {string} b
* @returns {number|undefined}
*/
export default function (a, b) {
if (a === b) {
return 0;
}

if (a.length > b.length) {
var tmp = a;
a = b;
b = tmp;
}

var la = a.length;
var lb = b.length;

while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) {
la--;
lb--;
}

var offset = 0;

while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) {
offset++;
}

la -= offset;
lb -= offset;

if (la === 0 || lb < 3) {
return lb;
}

var x = 0;
var y;
var d0;
var d1;
var d2;
var d3;
var dd;
var dy;
var ay;
var bx0;
var bx1;
var bx2;
var bx3;

var vector = [];

for (y = 0; y < la; y++) {
vector.push(y + 1);
vector.push(a.charCodeAt(offset + y));
}

var len = vector.length - 1;

for (; x < lb - 3; ) {
bx0 = b.charCodeAt(offset + (d0 = x));
bx1 = b.charCodeAt(offset + (d1 = x + 1));
bx2 = b.charCodeAt(offset + (d2 = x + 2));
bx3 = b.charCodeAt(offset + (d3 = x + 3));
dd = x += 4;
for (y = 0; y < len; y += 2) {
dy = vector[y];
ay = vector[y + 1];
d0 = _min(dy, d0, d1, bx0, ay);
d1 = _min(d0, d1, d2, bx1, ay);
d2 = _min(d1, d2, d3, bx2, ay);
dd = _min(d2, d3, dd, bx3, ay);
vector[y] = dd;
d3 = d2;
d2 = d1;
d1 = d0;
d0 = dy;
}
}

for (; x < lb; ) {
bx0 = b.charCodeAt(offset + (d0 = x));
dd = ++x;
for (y = 0; y < len; y += 2) {
dy = vector[y];
vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]);
d0 = dy;
}
}

return dd;
}
23 changes: 19 additions & 4 deletions packages/check-html-links/test-node/formatErrors.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import chai from 'chai';
import chalk from 'chalk';
import { execute } from './test-helpers.js';
import { formatErrors } from 'check-html-links';
import path from 'path';
import { execute } from './test-helpers.js';
import { listFiles } from '../src/listFiles.js';

const { expect } = chai;

async function executeAndFormat(inPath) {
const { errors, cleanup } = await execute(inPath);
return formatErrors(cleanup(errors));
const { errors, cleanup, testDir } = await execute(inPath);
const rootDir = path.resolve(testDir);
const files = await listFiles('**/*.html', rootDir);
return formatErrors(cleanup(errors), { files });
}

describe('formatErrors', () => {
Expand All @@ -16,23 +20,34 @@ describe('formatErrors', () => {
chalk.level = 0;
});

it('prints a nice summery', async () => {
it('prints a nice summary', async () => {
const result = await executeAndFormat('fixtures/test-case');
expect(result.trim().split('\n')).to.deep.equal([
'1. missing id="my-teams" in fixtures/test-case/price/index.html',
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
'',
'Suggestion: did you mean packages/check-html-links/test-node/fixtures/test-case/price/index.html instead?',
'',
'',
'2. missing file fixtures/test-case/about/images/team.png',
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
'',
'Suggestion: did you mean packages/check-html-links/test-node/fixtures/test-case/index.html instead?',
'',
'',
'3. missing reference target fixtures/test-case/aboot',
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
' ... 2 more references to this target',
'',
'Suggestion: did you mean packages/check-html-links/test-node/fixtures/test-case/index.html instead?',
'',
'',
'4. missing reference target fixtures/test-case/prce',
' from fixtures/test-case/index.html:1:9 via href="./prce"',
'',
'Suggestion: did you mean packages/check-html-links/test-node/fixtures/test-case/index.html instead?',
]);
});
});
1 change: 1 addition & 0 deletions packages/check-html-links/test-node/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ export async function execute(inPath, opts) {
return newItems;
},
errors,
testDir,
};
}
Loading