diff --git a/.remarkrc.mjs b/.remarkrc.mjs index 64af728..862a177 100644 --- a/.remarkrc.mjs +++ b/.remarkrc.mjs @@ -6,6 +6,7 @@ import remarkPresetLintRecommended from 'remark-preset-lint-recommended'; import remarkNoInlineCodeFences from './src/plugins/remark-no-inline-code-fences.mjs'; import remarkNoHtmlLinks from './src/plugins/remark-no-html-links.mjs'; import remarkLintNoDeadUrls from 'remark-lint-no-dead-urls'; +import remarkForceRootRelative from './src/plugins/remark-force-root-relative.mjs'; export default { plugins: [ @@ -28,5 +29,6 @@ export default { ], // Add known flaky URL patterns here }, ], + remarkForceRootRelative, ], }; diff --git a/astro.config.mjs b/astro.config.mjs index 5931855..c464235 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -8,6 +8,7 @@ import remarkImageAttributes from './src/plugins/remark-image-attributes'; import { remarkMdxGlobalImports } from './src/plugins/remark-mdx-global-imports'; import remarkCodeRegion from './src/plugins/remark-code-region'; import { unified } from '@astrojs/markdown-remark'; +import rehypeTargetBlank from './src/plugins/rehype-external-links'; export default defineConfig({ site: 'https://frcsoftware.org', @@ -23,6 +24,7 @@ export default defineConfig({ remarkMdxGlobalImports, remarkCodeRegion, ], + rehypePlugins: [rehypeTargetBlank], }), }, diff --git a/package.json b/package.json index d2793a6..b844146 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,14 @@ "@astrojs/check": "^0.9.9", "@eslint/js": "^10.0.1", "@types/estree": "^1.0.9", + "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", "astro-eslint-parser": "^1.4.0", "eslint": "^10.5.0", "eslint-plugin-astro": "^1.7.0", - "mdast-util-mdx": "^3.0.0", "husky": "^9.1.7", "lint-staged": "^17.0.8", + "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.2.0", "mdx2vast": "^0.3.1", "prettier": "^3.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03e40d4..55dce26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: '@types/estree': specifier: ^1.0.9 version: 1.0.9 + '@types/hast': + specifier: ^3.0.5 + version: 3.0.5 '@types/mdast': specifier: ^4.0.4 version: 4.0.4 @@ -1116,8 +1119,8 @@ packages: '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} @@ -4010,7 +4013,7 @@ snapshots: '@astrojs/internal-helpers@0.10.1': dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 js-yaml: 4.1.1 picomatch: 4.0.5 @@ -4113,7 +4116,7 @@ snapshots: '@astrojs/mdx': 7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.59.0)(tsx@4.22.4)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.2 '@pagefind/default-ui': 1.4.0 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 astro: 7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.59.0)(tsx@4.22.4)(yaml@2.9.0) @@ -4558,7 +4561,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdx': 2.0.13 acorn: 8.16.0 collapse-white-space: 2.1.0 @@ -4826,7 +4829,7 @@ snapshots: '@shikijs/primitive': 4.1.0 '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 '@shikijs/engine-javascript@4.1.0': @@ -4848,7 +4851,7 @@ snapshots: dependencies: '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@shikijs/themes@4.1.0': dependencies: @@ -4857,7 +4860,7 @@ snapshots: '@shikijs/types@4.1.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@shikijs/vscode-textmate@10.0.2': {} @@ -4899,7 +4902,7 @@ snapshots: dependencies: '@types/unist': 2.0.11 - '@types/hast@3.0.4': + '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 @@ -5453,7 +5456,7 @@ snapshots: dead-or-alive@1.0.5: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 fast-content-type-parse: 3.0.0 hast-util-from-html-isomorphic: 2.0.0 hast-util-is-element: 3.0.0 @@ -5918,12 +5921,12 @@ snapshots: hast-util-embedded@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-is-element: 3.0.0 hast-util-format@1.1.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-embedded: 3.0.0 hast-util-minify-whitespace: 1.0.1 hast-util-phrasing: 3.0.1 @@ -5933,20 +5936,20 @@ snapshots: hast-util-from-dom@5.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hastscript: 9.0.1 web-namespaces: 2.0.1 hast-util-from-html-isomorphic@2.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-from-dom: 5.0.1 hast-util-from-html: 2.0.3 unist-util-remove-position: 5.0.0 hast-util-from-html@2.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 devlop: 1.1.0 hast-util-from-parse5: 8.0.3 parse5: 7.3.0 @@ -5955,7 +5958,7 @@ snapshots: hast-util-from-parse5@8.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 devlop: 1.1.0 hastscript: 9.0.1 @@ -5966,19 +5969,19 @@ snapshots: hast-util-has-property@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-is-body-ok-link@3.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-is-element@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-minify-whitespace@1.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-embedded: 3.0.0 hast-util-is-element: 3.0.0 hast-util-whitespace: 3.0.0 @@ -5986,11 +5989,11 @@ snapshots: hast-util-parse-selector@4.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-phrasing@3.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-embedded: 3.0.0 hast-util-has-property: 3.0.0 hast-util-is-body-ok-link: 3.0.1 @@ -5998,7 +6001,7 @@ snapshots: hast-util-raw@9.1.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 '@ungap/structured-clone': 1.3.0 hast-util-from-parse5: 8.0.3 @@ -6014,7 +6017,7 @@ snapshots: hast-util-select@6.0.4: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 bcp-47-match: 2.0.3 comma-separated-tokens: 2.0.3 @@ -6054,7 +6057,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 estree-util-attach-comments: 3.0.0 @@ -6073,7 +6076,7 @@ snapshots: hast-util-to-html@9.0.5: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 @@ -6088,7 +6091,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 devlop: 1.1.0 @@ -6107,7 +6110,7 @@ snapshots: hast-util-to-parse5@8.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 property-information: 7.1.0 @@ -6117,11 +6120,11 @@ snapshots: hast-util-to-string@3.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-text@4.0.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 hast-util-is-element: 3.0.0 unist-util-find-after: 5.0.0 @@ -6130,11 +6133,11 @@ snapshots: hast-util-whitespace@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hastscript@9.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 property-information: 7.1.0 @@ -6556,7 +6559,7 @@ snapshots: mdast-util-math@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 longest-streak: 3.1.0 @@ -6579,7 +6582,7 @@ snapshots: mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -6607,7 +6610,7 @@ snapshots: mdast-util-mdx-jsx@3.2.0: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 ccount: 2.0.1 @@ -6654,7 +6657,7 @@ snapshots: mdast-util-mdxjs-esm@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -6685,7 +6688,7 @@ snapshots: mdast-util-to-hast@13.2.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@ungap/structured-clone': 1.3.0 devlop: 1.1.0 @@ -7623,38 +7626,38 @@ snapshots: rehype-format@5.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-format: 1.1.0 rehype-parse@9.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-from-html: 2.0.3 unified: 11.0.5 rehype-raw@7.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw: 9.1.0 vfile: 6.0.3 rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-estree: 3.1.3 transitivePeerDependencies: - supports-color rehype-stringify@10.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 unified: 11.0.5 rehype@13.0.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 rehype-parse: 9.0.1 rehype-stringify: 10.0.1 unified: 11.0.5 @@ -7899,7 +7902,7 @@ snapshots: remark-rehype@11.1.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 mdast-util-to-hast: 13.2.1 unified: 11.0.5 @@ -8051,7 +8054,7 @@ snapshots: satteri@0.9.5: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 optionalDependencies: @@ -8117,7 +8120,7 @@ snapshots: '@shikijs/themes': 4.1.0 '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 signal-exit@4.1.0: {} diff --git a/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx b/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx index 7ceb62d..38293f2 100644 --- a/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx +++ b/src/content/docs/learning-course/stage1/stage1b/stage-overview.mdx @@ -19,11 +19,11 @@ By the end of Stage 1B, you'll rewrite your Stage 1A kitbot code into a command- This stage will cover the following topics: -- [The Concepts of Command-Based Programming](https://frcsoftware.org/learning-course/stage1/stage1b/command-based-overview/) -- [The Body of a Command](https://frcsoftware.org/learning-course/stage1/stage1b/the-command-body/) -- [Commands & Mechanisms, Part 1](https://frcsoftware.org/learning-course/stage1/stage1b/commands-and-mechanisms/) -- [Triggers and Scheduling](https://frcsoftware.org/learning-course/stage1/stage1b/triggers/) -- [Commands and Mechanisms, Part 2](https://frcsoftware.org/learning-course/stage1/stage1b/commands-and-mechanisms-pt2/) -- [Exercise: Kitbot Rewrite, Pt 1](https://frcsoftware.org/learning-course/stage1/stage1b/command-based-kitbot/) -- [Suppliers in Command-Based](https://frcsoftware.org/learning-course/stage1/stage1b/suppliers-in-command-based/) -- [Exercise: Kitbot Rewrite, Pt 2](https://frcsoftware.org/learning-course/stage1/stage1b/command-based-kitbot-pt2/) +- [The Concepts of Command-Based Programming](/learning-course/stage1/stage1b/command-based-overview/) +- [The Body of a Command](/learning-course/stage1/stage1b/the-command-body/) +- [Commands & Mechanisms, Part 1](/learning-course/stage1/stage1b/commands-and-mechanisms/) +- [Triggers and Scheduling](/learning-course/stage1/stage1b/triggers/) +- [Commands and Mechanisms, Part 2](/learning-course/stage1/stage1b/commands-and-mechanisms-pt2/) +- [Exercise: Kitbot Rewrite, Pt 1](/learning-course/stage1/stage1b/command-based-kitbot/) +- [Suppliers in Command-Based](/learning-course/stage1/stage1b/suppliers-in-command-based/) +- [Exercise: Kitbot Rewrite, Pt 2](/learning-course/stage1/stage1b/command-based-kitbot-pt2/) diff --git a/src/plugins/rehype-external-links.ts b/src/plugins/rehype-external-links.ts new file mode 100644 index 0000000..a800b24 --- /dev/null +++ b/src/plugins/rehype-external-links.ts @@ -0,0 +1,21 @@ +import { visit } from 'unist-util-visit'; +import type { Root } from 'hast'; + +export default function rehypeTargetBlank() { + return (tree: Root) => { + visit(tree, 'element', (node) => { + if ( + node.tagName !== 'a' || + typeof node.properties.href !== 'string' || + !URL.canParse(node.properties.href) || + // only match http/s + !/^https?:/gm.test(URL.parse(node.properties.href)!.protocol) + ) { + // only run on valid external links + return; + } + node.properties.target = '_blank'; + node.properties.rel = ['noreferrer', 'noopener']; + }); + }; +} diff --git a/src/plugins/remark-force-root-relative.mjs b/src/plugins/remark-force-root-relative.mjs new file mode 100644 index 0000000..3d84937 --- /dev/null +++ b/src/plugins/remark-force-root-relative.mjs @@ -0,0 +1,25 @@ +import { visit } from 'unist-util-visit'; + +export default function remarkForceRootRelative() { + /** + * @param {import("mdast").Root} tree + * @param {import("vfile").VFile} file + */ + return (tree, file) => { + visit(tree, 'link', (node) => { + if (!URL.canParse(node.url)) { + // already root-relative, skip + return; + } + // can't use the site property from astro.config.mjs, see https://github.com/withastro/starlight/pull/3572 + const hostname = 'frcsoftware.org'; + const url = new URL(node.url); + if (url.hostname === hostname) { + file.message( + 'Use root-relative URL syntax for internal links.', + node, + ).fatal = true; + } + }); + }; +}