/** * Doc Link Checker * * We define here a test to make sure that there are no dead link in the Owl * documentation. */ // import * as fs from "fs"; // //-------------------------------------------------------------------------- // // Helpers // //-------------------------------------------------------------------------- // interface MarkDownLink { // name: string; // link: string; // } // interface MarkDownSection { // name: string; // slug: string; // } // interface FileData { // name: string; // path: string[]; // fullName: string; // links: MarkDownLink[]; // sections: MarkDownSection[]; // } // const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g; // const HEADING_REGEXP = /\n(#+\s*)(.*)/g; // export function addMardownData(fileData): void { // const sep = fileData.path.length > 0 ? "/" : ""; // const fullName = fileData.path.join("/") + sep + fileData.name; // const content = fs.readFileSync(fullName, { encoding: "utf8" }); // let m; // // get links info // do { // m = LINK_REGEXP.exec(content); // if (m) { // fileData.links.push({ name: m[0], link: m[2] }); // } // } while (m); // // get sections info // do { // m = HEADING_REGEXP.exec(content); // if (m) { // fileData.sections.push({ name: m[0], slug: slugify(m[2]) }); // } // } while (m); // } // /** // * Returns a list of FileData corresponding to all files that need to be // * validated. // */ // function getFiles(path: string[] = []): FileData[] { // if (path.length === 0) { // const baseFiles: FileData[] = [ // { name: "README.md", path: [], links: [], sections: [], fullName: "README.md" }, // { name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" }, // ]; // const rest = getFiles(["doc"]); // const result = baseFiles.concat(rest); // result.forEach(addMardownData); // return result; // } // const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map((f) => { // if (f.isDirectory()) { // return getFiles(path.concat(f.name)); // } // const fullName = path.join("/") + (path.length > 0 ? "/" : "") + f.name; // return [ // { // name: f.name, // path, // links: [], // sections: [], // fullName, // }, // ]; // }); // return Array.prototype.concat(...files); // } // const LOCAL_FILES = ["LICENSE", "tools/debug.js"]; // export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean { // if (link.link.startsWith("http")) { // // no check on external links // return true; // } // // Step 1: extract path, name, hash // // path = ['doc', 'architecture] // // name = 'rendering.md' // // hash = 'blabla' (or '' if no hash) // const parts = link.link.split("#"); // const hash = parts[1] || ""; // let name; // let path; // if (parts[0]) { // let temp = parts[0].split("/"); // name = temp[temp.length - 1]; // temp.splice(-1); // path = current.path.slice(); // for (let elem of temp) { // if (elem === "..") { // path.splice(-1); // } else if (elem !== ".") { // path.push(elem); // } // } // } else { // // there are no file name, so this is a relative link to the current file // name = current.name; // path = current.path; // } // // Step 2: build normalized link file name // const linkFullName = path.join("/") + (path.length > 0 ? "/" : "") + name; // // Step 3: check link name against white list of local files // if (LOCAL_FILES.includes(linkFullName)) { // return true; // } // // Step 4: check if there is a matching file // let target: FileData | undefined = files.find((f) => f.fullName === linkFullName); // if (!target) { // return false; // } // // Step 5: if necessary, check if there is a corresponding link inside the target // // link name // if (hash) { // if (!target.sections.find((s) => s.slug === hash)) { // return false; // } // } // return true; // } // // adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1 // function slugify(str) { // const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;"; // const b = "aaaaaaaaceeeeghiiiimnnnooooooprssstuuuuuwxyz-----"; // const p = new RegExp(a.split("").join("|"), "g"); // return str // .toString() // .toLowerCase() // .replace(/\//g, "") // remove / // .replace(/\s+/g, "-") // Replace spaces with - // .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters // .replace(/&/g, "-and-") // Replace & with ‘and’ // .replace(/[^\w\-]+/g, "") // Remove all non-word characters // .replace(/\-\-+/g, "-") // Replace multiple - with single - // .replace(/^-+/, "") // Trim - from start of text // .replace(/-+$/, ""); // Trim - from end of text // } //-------------------------------------------------------------------------- // Test //-------------------------------------------------------------------------- test.skip("All markdown links work", () => { // let linkNumber = 0; // let invalidLinkNumber = 0; // const data = getFiles(); // for (let file of data) { // for (let link of file.links) { // linkNumber++; // if (!isLinkValid(link, file, data)) { // console.warn(`Invalid Link: "${link.name}" in "${file.name}"`); // invalidLinkNumber++; // } // } // } // expect(invalidLinkNumber).toBe(0); // expect(linkNumber).toBeGreaterThan(10); });