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

Get yarn markdown-lint to work #21

Merged
merged 3 commits into from
Nov 5, 2024
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ data/*

# Migration node dependencies
.build

# remark node dependencies
.remark-build
81 changes: 81 additions & 0 deletions .remarkrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { resolve } from "path";
import remarkVariables from "./.remark-build/server/remark-variables.mjs";
import remarkIncludes from "./.remark-build/server/remark-includes.mjs";
import remarkTOC from "./.remark-build/server/remark-toc.mjs";
import { remarkLintTeleportDocsLinks } from "./.remark-build/server/lint-teleport-docs-links.mjs";
import {
getVersion,
getVersionRootPath,
} from "./.remark-build/server/docs-helpers.mjs";
import { loadConfig } from "./.remark-build/server/config-docs.mjs";
import {
updatePathsInIncludes,
} from "./.remark-build//server/asset-path-helpers.mjs";


const configFix = {
settings: {
bullet: "-",
ruleRepetition: 3,
fences: true,
incrementListMarker: true,
checkBlanks: true,
resourceLink: true,
emphasis: "*",
tablePipeAlign: false,
tableCellPadding: true,
listItemIndent: 1,
},
plugins: ["frontmatter", "mdx"],
};

const configLint = {
plugins: [
"frontmatter",
"mdx",
"preset-lint-markdown-style-guide",
["lint-table-pipe-alignment", false],
["lint-table-cell-padding", false],
["lint-maximum-line-length", false],
["lint-no-consecutive-blank-lines", false],
["lint-no-emphasis-as-heading", false],
["lint-fenced-code-flag", { allowEmpty: true }],
["lint-file-extension", false],
["lint-no-duplicate-headings", false],
["lint-list-item-spacing", { checkBlanks: true }],
["lint-no-shell-dollars", false],
["lint-list-item-indent", "space"],
["lint-ordered-list-marker-value", "single"],
["lint-maximum-heading-length", false],
["lint-no-shortcut-reference-link", false],
["lint-no-file-name-irregular-characters", false],
[remarkTOC],
[
remarkIncludes, // Lints (!include.ext!) syntax
{
lint: true,
rootDir: (vfile) => getVersionRootPath(vfile.path),
updatePaths: updatePathsInIncludes,
},
],
[
remarkVariables, // Lints (=variable=) syntax
{
lint: true,
variables: (vfile) => {
return loadConfig(getVersion(vfile.path)).variables || {};
},
},
],
// validate-links must be run after remarkVariables since some links
// include variables in their references, e.g.,
// [CM-08 Information System Component Inventory]((=fedramp.control_url=)CM-8)
["validate-links", { repository: false }],
[remarkLintTeleportDocsLinks],
// Disabling the remarkLintFrontmatter check until we fix
// gravitational/docs#80
// [remarkLintFrontmatter, ["error"]],
],
};

export default process.env.FIX ? configFix : configLint;
18 changes: 13 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"scripts": {
"spellcheck": "bash scripts/check-spelling.sh",
"build-remark": "rm -rf .remark-build && tsc --build tsconfig.node.json && tsc-esm-fix --target='.remark-build' --ext='.mjs'",
"git-update": "git submodule update --init --remote --progress --depth 1 --single-branch",
"prepare-files": "npx vite-node ./scripts/prepare-files.mts",
"prepare-sanity-data": "npx vite-node ./scripts/prepare-sanity-data.mts",
Expand All @@ -22,8 +23,7 @@
"base:prettier": "prettier '**/*.{js,jsx,ts,tsx,json}'",
"lint": "yarn base:eslint --fix && yarn base:prettier --write -l",
"lint-check": "yarn base:eslint && yarn base:prettier --check",
"markdown-lint": "remark --rc-path .remarkrc.mjs 'content/**/docs/pages/**/*.mdx' --quiet --frail --ignore-pattern '**/includes/**' --silently-ignore",
"markdown-lint-external-links": "WITH_EXTERNAL_LINKS=true yarn markdown-lint"
"markdown-lint": "yarn build-remark && remark --rc-path .remarkrc.mjs 'content/**/docs/pages/**/*.mdx' --quiet --frail --ignore-pattern '**/includes/**' --silently-ignore"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
Expand All @@ -49,8 +49,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-loadable": "^5.5.0",
"react-use": "^17.5.0",
"highlightjs-terraform": "https://github.com/highlightjs/highlightjs-terraform#eb1b9661e143a43dff6b58b391128ce5cdad31d4"
"react-use": "^17.5.0"
},
"devDependencies": {
"@csstools/postcss-global-data": "^2.1.1",
Expand All @@ -77,8 +76,17 @@
"postcss-preset-env": "^9.5.14",
"rehype-highlight": "^7.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-cli": "10.0.1",
"remark-copy-linked-files": "^1.5.0",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"remark-mdx": "^2.1.1",
"remark-parse": "^10.0.1",
"remark-preset-lint-markdown-style-guide": "^5.1.2",
"remark-rehype": "^10.1.0",
"remark-validate-links": "^11.0.2",
"to-vfile": "^8.0.0",
"tsc-esm-fix": "^3.1.0",
"tsm": "^2.3.0",
"typescript": "~5.5.2",
"unified": "^11.0.5",
Expand Down
2 changes: 1 addition & 1 deletion scripts/prepare-files.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { copyFileSync, rmSync, existsSync, mkdirSync } from "fs";
import { resolve, dirname } from "path";
import glob from "glob";
import { glob } from "glob";
import { docusaurusifyNavigation } from "../server/config-docs";
import {
getCurrentVersion,
Expand Down
58 changes: 46 additions & 12 deletions server/asset-path-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Root, RootContent } from "mdast";
import type { Parent, Image, Link, Definition } from "mdast";
import type { Node } from "unist";
import type { VFile } from "vfile";
import { resolve, relative, dirname } from "path";
import { getCurrentVersion, getLatestVersion } from "./config-site";
Expand All @@ -7,7 +8,12 @@ import { isLocalAssetFile } from "../src/utils/url";
const current = getCurrentVersion();
const latest = getLatestVersion();

const REGEXP_VERSION = /^\/versioned_docs\/version-([^\/]+)\//;
// The directory path pattern for versioned content transformed by the migration
// script
const REGEXP_POST_PREPARE_VERSION = /^\/versioned_docs\/version-([^\/]+)\//;
// The directory path pattern for versioned content not yet transformed by the
// migration script
const REGEXP_PRE_PREPARE_VERSION = /^\/?content\/([^\/]+)\//;
const REGEXP_EXTENSION = /(\/index)?\.mdx$/;

export type DocsMeta = {
Expand All @@ -27,20 +33,42 @@ const getProjectPath = (vfile: VFile) => vfile.path.replace(process.cwd(), "");

const isCurrent = (vfile: VFile) => getProjectPath(vfile).startsWith("/docs/");

// getVersionFromVFile extracts the docs version of a post-migration docs page
// so we can find the appropriate pre-migration version. If the docs page is
// already in the pre-migration directory, return the version number of that
// directory.
export const getVersionFromVFile = (vfile: VFile): string => {
return isCurrent(vfile)
? current
: REGEXP_VERSION.exec(getProjectPath(vfile))[1];
if (isCurrent(vfile)) {
return current;
}
const projectPath = getProjectPath(vfile);

const postPrepVersion = REGEXP_POST_PREPARE_VERSION.exec(projectPath);
if (!!postPrepVersion) {
return postPrepVersion[1];
}

const prePrepVersion = REGEXP_PRE_PREPARE_VERSION.exec(projectPath);
if (!!prePrepVersion) {
return prePrepVersion[1];
}

throw new Error(`unable to extract a version from filepath ${projectPath}`);
};

export const getRootDir = (vfile: VFile): string => {
return resolve("content", getVersionFromVFile(vfile));
};

const getCurrentDir = (vfile: VFile) =>
isCurrent(vfile)
const getCurrentDir = (vfile: VFile) => {
// The page is in the pre-migration directory, i.e., we're linting it
if (vfile.path.startsWith("content")) {
return resolve(`content/${getVersionFromVFile(vfile)}/docs/pages`);
}
return isCurrent(vfile)
? resolve("docs")
: resolve(`versioned_docs/version-${getVersionFromVFile(vfile)}`);
};

const getPagesDir = (vfile: VFile): string =>
resolve(getRootDir(vfile), "docs/pages");
Expand Down Expand Up @@ -85,7 +113,7 @@ export const updatePathsInIncludes = ({
includePath,
vfile,
}: {
node: Root | RootContent;
node: Node;
versionRootDir: string;
includePath: string;
vfile: VFile;
Expand All @@ -95,7 +123,7 @@ export const updatePathsInIncludes = ({
node.type === "link" ||
node.type === "definition"
) {
const href = node.url;
const href = (node as Link | Image | Definition).url;

// Ignore non-strings, absolute paths, web URLs, and links consisting only
// of anchors (these will end up pointing to the containing page).
Expand All @@ -117,18 +145,24 @@ export const updatePathsInIncludes = ({
href
).replace(getPagesDir(vfile), getCurrentDir(vfile));

node.url = relative(absMdxPath, absTargetPath);
(node as Link | Image | Definition).url = relative(
absMdxPath,
absTargetPath
);
} else {
const absMdxPath = resolve(getOriginalPath(vfile));

const absTargetPath = resolve(versionRootDir, dirname(includePath), href);

node.url = relative(dirname(absMdxPath), absTargetPath);
(node as Link | Image | Definition).url = relative(
dirname(absMdxPath),
absTargetPath
);
}
}

if ("children" in node) {
node.children?.forEach?.((child) =>
(node as Parent).children?.forEach?.((child) =>
updatePathsInIncludes({ node: child, versionRootDir, includePath, vfile })
);
}
Expand Down
21 changes: 21 additions & 0 deletions server/docs-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { resolve } from "path";

export const getVersion = (filepath: string) => {
const result = /content\/([^/]+)\/docs\//.exec(filepath);
return result ? result[1] : "";
};

/**
* Used by some remark plugins to resolve paths to assets based on the
* current docs folders. E. g. remark-includes.
*/
export const getVersionRootPath = (filepath: string) => {
const version = getVersion(filepath);

if (version) {
return resolve(`content/${version}`);
} else {
// CI task for linting stored files in the root of the content folder
return resolve("content");
}
};
62 changes: 62 additions & 0 deletions server/lint-teleport-docs-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Node } from "unist";
import type { Transformer } from "unified";
import type { Root, Link as MdastLink } from "mdast";
import type { EsmNode, MdxAnyElement } from "./types-unist";

import { visit } from "unist-util-visit";
import { isExternalLink, isHash, isPage } from "../utils/url";

interface ObjectHref {
src: string;
}

type Href = string | ObjectHref;

const mdxNodeTypes = new Set(["mdxJsxFlowElement", "mdxJsxTextElement"]);

const isMdxComponentWithHref = (node: Node): node is MdxAnyElement => {
return (
mdxNodeTypes.has(node.type) &&
(node as MdxAnyElement).attributes.some(
({ name, value }) => name === "href"
)
);
};

const isAnAbsoluteDocsLink = (href: string): boolean => {
return (
href.startsWith("/docs") || href.startsWith("https://goteleport.com/docs")
);
};

export function remarkLintTeleportDocsLinks(): Transformer {
return (root: Root, vfile) => {
visit(root, (node: Node) => {
if (
node.type == "link" &&
isAnAbsoluteDocsLink((node as MdastLink).url)
) {
vfile.message(
`Link reference ${
(node as MdastLink).url
} must be a relative link to an *.mdx page`,
node.position
);
return;
}

if (isMdxComponentWithHref(node)) {
const hrefAttribute = node.attributes.find(
({ name }) => name === "href"
);

if (isAnAbsoluteDocsLink(hrefAttribute.value as string)) {
vfile.message(
`Component href ${hrefAttribute.value} must be a relative link to an *.mdx page`,
node.position
);
}
}
});
};
}
Loading
Loading