From 7874dee996d643f59bccccacb4486059ee22cee2 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 1 May 2024 22:01:11 +0100 Subject: [PATCH 1/2] chore: add sinon dev-dependency for spying on console log --- package-lock.json | 203 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 204 insertions(+) diff --git a/package-lock.json b/package-lock.json index 26aaa55..c6ffc74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@11ty/eleventy": "^2.0.0", "ava": "^5.2.0", "c8": "^7.12.0", + "sinon": "^17.0.1", "slugify": "^1.6.6" }, "engines": { @@ -308,6 +309,50 @@ "node": ">=8" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1209,6 +1254,15 @@ "node": ">= 0.8.0" } }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2258,6 +2312,12 @@ "node": ">=0.10.0" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2349,6 +2409,12 @@ "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2674,6 +2740,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "node_modules/nofilter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", @@ -3528,6 +3607,24 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -3760,6 +3857,15 @@ "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", "dev": true }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -4325,6 +4431,52 @@ } } }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -4995,6 +5147,12 @@ "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", "dev": true }, + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -5760,6 +5918,12 @@ "integrity": "sha512-3KF80UaaSSxo8jVnRYtMKNGFOoVPBdkkVPsw+Ad0y4oxKXPduS6G6iHkrf69yJVff/VAaYXkV42rtZ7daJxU3w==", "dev": true }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5822,6 +5986,12 @@ "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6062,6 +6232,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "nofilter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", @@ -6681,6 +6864,20 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + } + }, "slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -6843,6 +7040,12 @@ "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", "dev": true }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", diff --git a/package.json b/package.json index c35e834..b3b8a13 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@11ty/eleventy": "^2.0.0", "ava": "^5.2.0", "c8": "^7.12.0", + "sinon": "^17.0.1", "slugify": "^1.6.6" }, "directories": { From ae44b1a4121059a24dc3e6ae53906f174758a749 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 1 May 2024 23:08:09 +0100 Subject: [PATCH 2/2] feat(#23): add detailed bad link report --- index.js | 2 +- src/dead-links.js | 41 +++++++++++++++++++ src/html-link-parser.js | 29 ++++++++++--- src/interlinker.js | 28 ++++++------- src/wikilink-parser.js | 14 ++++--- tests/eleventy.test.js | 40 +++++++++++++++++- .../_layouts/default.liquid | 1 + .../website-with-broken-links/about.md | 6 +++ .../eleventy.config.js | 12 ++++++ .../website-with-broken-links/hello.liquid | 6 +++ .../website-with-broken-links/something.md | 6 +++ tests/html-internal-link-parser.test.js | 9 +++- 12 files changed, 164 insertions(+), 30 deletions(-) create mode 100644 src/dead-links.js create mode 100644 tests/fixtures/website-with-broken-links/_layouts/default.liquid create mode 100644 tests/fixtures/website-with-broken-links/about.md create mode 100644 tests/fixtures/website-with-broken-links/eleventy.config.js create mode 100644 tests/fixtures/website-with-broken-links/hello.liquid create mode 100644 tests/fixtures/website-with-broken-links/something.md diff --git a/index.js b/index.js index 7a475cc..aba39fb 100644 --- a/index.js +++ b/index.js @@ -44,7 +44,7 @@ module.exports = function (eleventyConfig, options = {}) { // anything. // TODO: 1.1.0 have this contain more details such as which file(s) are linking (#23) // TODO: 1.1.0 have this clear the interlinker cache so that next time 11ty builds its starting from fresh data! (#24) - eleventyConfig.on('eleventy.after', () => interlinker.deadLinksReport()); + eleventyConfig.on('eleventy.after', () => interlinker.deadLinks.report()); // Teach Markdown-It how to display MediaWiki Links. eleventyConfig.amendLibrary('md', (md) => { diff --git a/src/dead-links.js b/src/dead-links.js new file mode 100644 index 0000000..f332440 --- /dev/null +++ b/src/dead-links.js @@ -0,0 +1,41 @@ +const chalk = require("chalk"); +module.exports = class DeadLinks { + + constructor() { + this.gravestones = new Map; + this.fileSrc = 'unknown'; + } + + setFileSrc(fileSrc) { + this.fileSrc = fileSrc; + } + + /** + * @param {string} link + */ + add(link) { + if (!this.fileSrc) this.fileSrc = 'unknown'; + + const names = this.gravestones.has(link) + ? this.gravestones.get(this.fileSrc) + : []; + + names.push(this.fileSrc) + + this.gravestones.set(link, names); + } + + report() { + for (const [link, files] of this.gravestones.entries()) { + console.warn( + chalk.blue('[@photogabble/wikilinks]'), + chalk.yellow('WARNING'), + `${(link.includes('href') ? 'Link' : 'Wikilink')} (${link}) found pointing to to non-existent page in:` + ); + + for (const file of files) { + console.warn(`\t- ${file}`); + } + } + } +} diff --git a/src/html-link-parser.js b/src/html-link-parser.js index 91b9a96..9c992d8 100644 --- a/src/html-link-parser.js +++ b/src/html-link-parser.js @@ -7,40 +7,57 @@ module.exports = class HTMLLinkParser { */ internalLinkRegex = /href="\/(.*?)"/g; + /** + * @param { DeadLinks } deadLinks + */ + constructor(deadLinks) { + this.deadLinks = deadLinks; + } + /** * Parses a single HTML link into the link object understood by the Interlinker. Unlike with Wikilinks we only * care about the href so that we can look up the linked page record. * * @param {string} link + * @param {import('@photogabble/eleventy-plugin-interlinker').PageDirectoryService} pageDirectory * @return {import('@photogabble/eleventy-plugin-interlinker').LinkMeta} */ - parseSingle(link) { - return { + parseSingle(link, pageDirectory) { + const meta = { href: link.slice(6, -1) .replace(/.(md|markdown)\s?$/i, "") .replace("\\", "") .trim() .split("#")[0], isEmbed: false, + }; + + if (!pageDirectory.findByLink(meta)) { + this.deadLinks.add(link); } + + return meta; } /** * @param {Array} links + * @param {import('@photogabble/eleventy-plugin-interlinker').PageDirectoryService} pageDirectory * @return {Array} */ - parseMultiple(links) { - return links.map(link => this.parseSingle(link)); + parseMultiple(links, pageDirectory) { + return links.map(link => this.parseSingle(link, pageDirectory)); } /** * Find's all internal href links within an HTML document and returns the parsed result. * @param {string} document + * @param {import('@photogabble/eleventy-plugin-interlinker').PageDirectoryService} pageDirectory * @return {Array} */ - find(document) { + find(document, pageDirectory) { return this.parseMultiple( - (document.match(this.internalLinkRegex) || []) + (document.match(this.internalLinkRegex) || []), + pageDirectory ) } } diff --git a/src/interlinker.js b/src/interlinker.js index 7fcbfce..19ff4b6 100644 --- a/src/interlinker.js +++ b/src/interlinker.js @@ -1,8 +1,9 @@ -const {EleventyRenderPlugin} = require("@11ty/eleventy"); -const WikilinkParser = require("./wikilink-parser"); const HTMLLinkParser = require("./html-link-parser"); -const chalk = require("chalk"); +const WikilinkParser = require("./wikilink-parser"); +const {EleventyRenderPlugin} = require("@11ty/eleventy"); +const DeadLinks = require("./dead-links"); const {pageLookup} = require("./find-page"); +const chalk = require("chalk"); /** * Interlinker: @@ -13,7 +14,7 @@ module.exports = class Interlinker { this.opts = opts // Set of WikiLinks pointing to non-existent pages - this.deadWikiLinks = new Set(); + this.deadLinks = new DeadLinks(); // Map of what WikiLinks to what this.linkMapCache = new Map(); @@ -28,14 +29,16 @@ module.exports = class Interlinker { // TODO: document this.rm = new EleventyRenderPlugin.RenderManager(); - this.wikiLinkParser = new WikilinkParser(opts, this.deadWikiLinks); - this.HTMLLinkParser = new HTMLLinkParser(); + this.wikiLinkParser = new WikilinkParser(opts, this.deadLinks); + + // TODO: pass through deadWikiLinks and have HTMLLinkParser look up pages + this.HTMLLinkParser = new HTMLLinkParser(this.deadLinks); } /** * Compiles the template associated with a WikiLink when invoked via the ![[embed syntax]] * @param data - * @return {Promise} + * @return {Promise} */ async compileTemplate(data) { if (this.compiledEmbeds.has(data.url)) return; @@ -94,6 +97,8 @@ module.exports = class Interlinker { let currentSlugs = new Set([currentSlug, data.page.fileSlug]); const currentPage = pageDirectory.findByFile(data); + this.deadLinks.setFileSrc(currentPage.inputPath); + // Populate our link map for use later in replacing WikiLinks with page permalinks. // Pages can list aliases in their front matter, if those exist we should map them // as well. @@ -124,10 +129,11 @@ module.exports = class Interlinker { const pageContent = currentPage.template.frontMatter.content; const outboundLinks = [ ...this.wikiLinkParser.find(pageContent, pageDirectory), - ...this.HTMLLinkParser.find(pageContent), + ...this.HTMLLinkParser.find(pageContent, pageDirectory), ].map((link) => { // Lookup the page this link, links to and add this page to its backlinks const page = pageDirectory.findByLink(link); + if (!page) return link; if (!page.data.backlinks) { page.data.backlinks = []; @@ -156,10 +162,4 @@ module.exports = class Interlinker { return []; } - - deadLinksReport() { - this.deadWikiLinks.forEach( - slug => console.warn(chalk.blue('[@photogabble/wikilinks]'), chalk.yellow('WARNING'), `WikiLink found pointing to non-existent [${slug}], has been set to default stub.`) - ) - } } diff --git a/src/wikilink-parser.js b/src/wikilink-parser.js index e758a1a..f13cce7 100644 --- a/src/wikilink-parser.js +++ b/src/wikilink-parser.js @@ -7,18 +7,22 @@ module.exports = class WikilinkParser { wikiLinkRegExp = /(?} deadWikiLinks * @param { import('@photogabble/eleventy-plugin-interlinker').EleventyPluginInterlinkOptions } opts + * @param { DeadLinks } deadLinks */ - constructor(opts, deadWikiLinks) { + constructor(opts, deadLinks) { this.opts = opts; this.slugifyFn = opts.slugifyFn; - this.deadWikiLinks = deadWikiLinks; + this.deadLinks = deadLinks; // TODO: when 11ty is in serve mode, this cache should clear at the beginning of each build (#24) this.linkCache = new Map(); } + setPage(page) { + this.page = page; + } + /** * Parses a single WikiLink into the link object understood by the Interlinker. * @@ -68,9 +72,9 @@ module.exports = class WikilinkParser { meta.href = page.url; meta.path = page.inputPath; } else { - // If this wikilink goes to a page that doesn't exist, add to deadWikiLinks list and + // If this wikilink goes to a page that doesn't exist, add to deadLinks list and // update href for stub post. - this.deadWikiLinks.add(link); + this.deadLinks.add(link); // @todo make the stub post url configurable, or even able to be disabled. (#25) meta.href = '/stubs'; } diff --git a/tests/eleventy.test.js b/tests/eleventy.test.js index a569bd3..6da9b9a 100644 --- a/tests/eleventy.test.js +++ b/tests/eleventy.test.js @@ -1,7 +1,8 @@ -const test = require("ava"); const Eleventy = require("@11ty/eleventy"); -const {normalize} = require('./helpers'); const path = require('node:path'); +const {normalize} = require('./helpers'); +const sinon = require('sinon'); +const test = require("ava"); function findResultByUrl(results, url) { const [result] = results.filter(result => result.url === url); @@ -29,3 +30,38 @@ test("Sample page (wikilinks and regular links)", async t => { '
This is to show that we can link back via regular internal links.
', ); }); + +test("Broken page (wikilinks and regular links)", async t => { + const messages = []; + const consoleStub = sinon.stub(console, 'warn').callsFake(msg => messages.push()); + + let elev = new Eleventy(fixturePath('website-with-broken-links'), fixturePath('website-with-broken-links/_site'), { + configPath: fixturePath('website-with-broken-links/eleventy.config.js'), + }); + + let results = await elev.toJSON(); + + let logLines = []; + for (let i = 0; i < consoleStub.callCount; i++) { + const line = normalize(consoleStub.getCall(i).args.join(' ')) + // Sometimes 11ty will output benchmark info, failing the test randomly. + if (line.includes('[11ty]')) continue; + logLines.push(line); + } + + consoleStub.restore(); + + // Markdown will have the link href set to /stubs + t.is( + normalize(findResultByUrl(results, '/something/').content), + `

This page has a broken link.

` + ); + + // HTML + t.is( + normalize(findResultByUrl(results, '/hello/').content), + `
This is to show that we can identify broken internal links.
` + ); + + t.is(logLines.length, 4, 'console.warn should be called three times'); +}); diff --git a/tests/fixtures/website-with-broken-links/_layouts/default.liquid b/tests/fixtures/website-with-broken-links/_layouts/default.liquid new file mode 100644 index 0000000..c9bcd4c --- /dev/null +++ b/tests/fixtures/website-with-broken-links/_layouts/default.liquid @@ -0,0 +1 @@ +
{{ content }}
diff --git a/tests/fixtures/website-with-broken-links/about.md b/tests/fixtures/website-with-broken-links/about.md new file mode 100644 index 0000000..17a26a6 --- /dev/null +++ b/tests/fixtures/website-with-broken-links/about.md @@ -0,0 +1,6 @@ +--- +title: About +layout: default.liquid +--- + +This is to show that we can link between Markdown files and [[hello|liquid files]]. diff --git a/tests/fixtures/website-with-broken-links/eleventy.config.js b/tests/fixtures/website-with-broken-links/eleventy.config.js new file mode 100644 index 0000000..9b10b5c --- /dev/null +++ b/tests/fixtures/website-with-broken-links/eleventy.config.js @@ -0,0 +1,12 @@ +module.exports = function (eleventyConfig) { + eleventyConfig.addPlugin( + require('../../../index.js'), + ); + + return { + dir: { + includes: "_includes", + layouts: "_layouts", + } + } +} diff --git a/tests/fixtures/website-with-broken-links/hello.liquid b/tests/fixtures/website-with-broken-links/hello.liquid new file mode 100644 index 0000000..439c776 --- /dev/null +++ b/tests/fixtures/website-with-broken-links/hello.liquid @@ -0,0 +1,6 @@ +--- +title: Hello +layout: default.liquid +--- + +This is to show that we can identify broken internal links. diff --git a/tests/fixtures/website-with-broken-links/something.md b/tests/fixtures/website-with-broken-links/something.md new file mode 100644 index 0000000..e0a1dd2 --- /dev/null +++ b/tests/fixtures/website-with-broken-links/something.md @@ -0,0 +1,6 @@ +--- +title: Something +layout: default.liquid +--- + +This page has a [[broken link]]. diff --git a/tests/html-internal-link-parser.test.js b/tests/html-internal-link-parser.test.js index 3fc9d2f..d8f0901 100644 --- a/tests/html-internal-link-parser.test.js +++ b/tests/html-internal-link-parser.test.js @@ -1,9 +1,14 @@ const HTMLLinkParser = require('../src/html-link-parser'); const test = require('ava'); +const DeadLinks = require("../src/dead-links"); +const {pageLookup} = require("../src/find-page"); +const slugify = require("slugify"); + +const pageDirectory = pageLookup([], slugify); test('html link parser grabs multiple href, ignoring external links', t => { - const parser = new HTMLLinkParser(); - const links = parser.find('

Hello world this is a link home and this is a link somewhere

The following link should be ignored example.com.

'); + const parser = new HTMLLinkParser(new DeadLinks()); + const links = parser.find('

Hello world this is a link home and this is a link somewhere

The following link should be ignored example.com.

', pageDirectory); t.is(2, links.length);