From 9de814cc71fdf1f265fc38c8e59cafe2a9e0b914 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Thu, 2 Jul 2026 18:54:02 +0200 Subject: [PATCH 1/2] feature: link mapping plugin --- .../questions/_mfa-weitere-informationen.mdx | 6 +- docs/link-mappings.yaml | 3 + packages/ict/link-mapping/README.mdx | 19 ++++++ packages/ict/link-mapping/index.ts | 66 +++++++++++++++++++ packages/ict/link-mapping/package.json | 19 ++++++ .../ict/link-mapping/tests/plugin.test.ts | 38 +++++++++++ packages/ict/link-mapping/tsconfig.json | 3 + siteConfig.ts | 9 ++- 8 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 docs/link-mappings.yaml create mode 100644 packages/ict/link-mapping/README.mdx create mode 100644 packages/ict/link-mapping/index.ts create mode 100644 packages/ict/link-mapping/package.json create mode 100644 packages/ict/link-mapping/tests/plugin.test.ts create mode 100644 packages/ict/link-mapping/tsconfig.json diff --git a/docs/03-support/01-faq/questions/_mfa-weitere-informationen.mdx b/docs/03-support/01-faq/questions/_mfa-weitere-informationen.mdx index db2b101e..2fffee1c 100644 --- a/docs/03-support/01-faq/questions/_mfa-weitere-informationen.mdx +++ b/docs/03-support/01-faq/questions/_mfa-weitere-informationen.mdx @@ -1,7 +1,9 @@ #### Wo finde ich weitere Informationen und Anleitungen zur __MFA__? - - Eine detaillierte Anleitung von EDUBERN finden Sie hier: [EDUBERN: Anleitung MFA](https://erzbe.sharepoint.com/sites/EDUBERN-Infohub-Hilfsmittel/Lists/Hilfsmittel/Attachments/4/Anleitung%20-%20Einrichten%20MFA%20EDUBERN%20.pdf). + Offizielle Anleitungen von EDUBERN finden Sie hier: + - [EDUBERN Kurzanleitung Account & MFA](@office-onboarding-short) + - [EDUBERN detaillierte Anleitung Account & MFA](@office-onboarding-long) + - [EDUBERN Anleitung MFA ohne Handy](@mfa-ohne-handy) \ No newline at end of file diff --git a/docs/link-mappings.yaml b/docs/link-mappings.yaml new file mode 100644 index 00000000..4a593354 --- /dev/null +++ b/docs/link-mappings.yaml @@ -0,0 +1,3 @@ +mfa-ohne-handy: "https://erzbe.sharepoint.com/sites/EDUBERN-Infohub-Hilfsmittel/Lists/Hilfsmittel/Attachments/21/Anleitung_MFA_ohne_Handy.pdf" +office-onboarding-short: "https://erzbe.sharepoint.com/sites/EDUBERN-Infohub-Hilfsmittel/Lists/Hilfsmittel/Attachments/157/Anleitung%20%e2%80%93%20Onboarding%20EDUBERN_BYOD_RESET_D_SHORT.pdf" +office-onboarding-long: "https://erzbe.sharepoint.com/sites/EDUBERN-Infohub-Hilfsmittel/Lists/Hilfsmittel/Attachments/157/Anleitung%20%e2%80%93%20Onboarding%20EDUBERN%20BYOD%20FED_RESET_D.pdf" diff --git a/packages/ict/link-mapping/README.mdx b/packages/ict/link-mapping/README.mdx new file mode 100644 index 00000000..9fbd1607 --- /dev/null +++ b/packages/ict/link-mapping/README.mdx @@ -0,0 +1,19 @@ +--- +page_id: 979630e9-9272-410b-a7c1-fe40601be7a4 +--- + +# Link Mapping + + +Mit `@ict/link-mapping` lassen sich links in einer `.yaml`-Datei festlegen und dann als Markdown-Link verwenden. + +```yaml +foo: https://foo.com +bar: https://bar.com +``` + +```md +Auf [foo](@foo) sieht man einen Link zu [bar](@bar). +``` + +Das Schlüsselwort muss mit `@` beginnen, damit es als Link erkannt wird. diff --git a/packages/ict/link-mapping/index.ts b/packages/ict/link-mapping/index.ts new file mode 100644 index 00000000..4c403ec1 --- /dev/null +++ b/packages/ict/link-mapping/index.ts @@ -0,0 +1,66 @@ +import { visit } from 'unist-util-visit'; +import type { Plugin, Transformer } from 'unified'; +import { Root, Text } from 'mdast'; +import * as yaml from 'js-yaml'; +import * as fs from 'fs/promises'; +import path from 'path'; + +interface OptionsInput { + mapping: Record; + mappingFilePath?: string; +} + +const links = new Map(); +let lastModifiedTime = 0; + +const loadMapping = async (mappingFilePath?: string) => { + if (!mappingFilePath) { + return; + } + // get time of last modification of the mapping file + const stats = await fs.stat(mappingFilePath).catch((err) => { + return { mtimeMs: 0 }; + }); + const lastModified = stats.mtimeMs; + + if (lastModified <= lastModifiedTime) { + return; + } + if (lastModified > lastModifiedTime) { + lastModifiedTime = lastModified; + const fileContent = await fs.readFile(mappingFilePath, 'utf8').catch((err) => { + console.error(`Error reading mapping file at ${mappingFilePath}:`, err); + return ''; + }); + const mapping = yaml.load(fileContent) as Record; + // don't clear the links map, instead just update it with new values + // links.clear(); + for (const [key, value] of Object.entries(mapping)) { + links.set(key, value); + } + } +}; + +const plugin: Plugin = function plugin( + optionsInput = { mapping: {} } +): Transformer { + for (const [key, value] of Object.entries(optionsInput.mapping ?? {})) { + links.set(key, value); + } + + return async (tree) => { + await loadMapping(optionsInput.mappingFilePath); + visit(tree, 'link', (node) => { + if (!node.url.startsWith('@')) { + return; + } + const translationKey = node.url.slice(1); + if (!links.has(translationKey)) { + return; + } + node.url = links.get(translationKey) ?? node.url; + }); + }; +}; + +export default plugin; diff --git a/packages/ict/link-mapping/package.json b/packages/ict/link-mapping/package.json new file mode 100644 index 00000000..cb561fc4 --- /dev/null +++ b/packages/ict/link-mapping/package.json @@ -0,0 +1,19 @@ +{ + "name": "@ict/link-mapping", + "version": "1.0.0", + "main": "index.ts", + "types": "index.ts", + "dependencies": { + + }, + "devDependencies": { + "vitest": "*", + "@docusaurus/module-type-aliases": "*", + "@docusaurus/core": "*" + }, + "peerDependencies": { + "@tdev/core": "1.0.0", + "js-yaml": "*", + "@types/js-yaml": "*" + } +} diff --git a/packages/ict/link-mapping/tests/plugin.test.ts b/packages/ict/link-mapping/tests/plugin.test.ts new file mode 100644 index 00000000..ef718f8e --- /dev/null +++ b/packages/ict/link-mapping/tests/plugin.test.ts @@ -0,0 +1,38 @@ +import { remark } from 'remark'; +import remarkMdx from 'remark-mdx'; +import remarkDirective from 'remark-directive'; +import { describe, expect, it } from 'vitest'; + +const process = async (content: string, mapping: Record = {}) => { + const { default: plugin } = await import('..'); + const result = await remark() + .use(remarkMdx) + .use(remarkDirective) + .use(plugin, { mapping }) + .process(content); + + return result.value; +}; + +describe('#link annotation', () => { + it("does nothing if there's no link", async () => { + const input = `# Heading + +Some content +`; + const result = await process(input); + expect(result).toBe(input); + }); + it('can map links', async () => { + const input = `# Details element example + Hello [example](@translation-key) world! + `; + const result = await process(input, { ['translation-key']: 'https://example.org' }); + expect(result).toMatchInlineSnapshot(` + "# Details element example + + Hello [example](https://example.org) world! + " + `); + }); +}); diff --git a/packages/ict/link-mapping/tsconfig.json b/packages/ict/link-mapping/tsconfig.json new file mode 100644 index 00000000..ab140871 --- /dev/null +++ b/packages/ict/link-mapping/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../tsconfig.package.json" +} diff --git a/siteConfig.ts b/siteConfig.ts index 9e2550a7..eeae2d98 100644 --- a/siteConfig.ts +++ b/siteConfig.ts @@ -7,6 +7,7 @@ import { recommendedBeforeDefaultRemarkPlugins, recommendedRemarkPlugins } from './src/siteConfig/markdownPluginConfigs'; +import linkMappingRemarkPlugin from './packages/ict/link-mapping'; import { devModeAccessLocalFS, taskStateOverview } from './src/siteConfig/navbarItems'; import { SiteConfigProvider } from './src/siteConfig/siteConfig'; import detailsPlugin from './src/plugins/remark-details/plugin'; @@ -21,6 +22,7 @@ const ADMONITION_CONFIG = { }; import dynamicRouter from './src/plugins/plugin-dynamic-routes'; import { type DirectusConfig } from '@ict/directus'; +import path from 'path'; declare module './src/siteConfig/siteConfig' { export interface TdevConfig { @@ -139,6 +141,7 @@ const getSiteConfig: SiteConfigProvider = () => { } ], beforeDefaultRemarkPlugins: [ + [linkMappingRemarkPlugin, { mappingFilePath: './docs/link-mappings.yaml' }], ...recommendedBeforeDefaultRemarkPlugins.filter((p) => p !== detailsPluginConfig), [ detailsPlugin, @@ -154,9 +157,9 @@ const getSiteConfig: SiteConfigProvider = () => { } ] ] as unknown as PluginOptions[], - remarkPlugins: recommendedRemarkPlugins.filter( - (p) => p !== commentPluginConfig - ) as unknown as PluginOptions[], + remarkPlugins: [ + ...recommendedRemarkPlugins.filter((p) => p !== commentPluginConfig) + ] as unknown as PluginOptions[], apiDocumentProviders: [ require.resolve('@tdev/page-read-check/register'), require.resolve('@ict/directus/register') From 6766453e25933d9eb1d9e62f06f92bc718c0013d Mon Sep 17 00:00:00 2001 From: bh0fer Date: Thu, 2 Jul 2026 19:01:17 +0200 Subject: [PATCH 2/2] update readme --- packages/ict/link-mapping/README.mdx | 24 ++++++++++++++++++++++-- siteConfig.ts | 5 ++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/ict/link-mapping/README.mdx b/packages/ict/link-mapping/README.mdx index 9fbd1607..32ed675f 100644 --- a/packages/ict/link-mapping/README.mdx +++ b/packages/ict/link-mapping/README.mdx @@ -4,7 +4,6 @@ page_id: 979630e9-9272-410b-a7c1-fe40601be7a4 # Link Mapping - Mit `@ict/link-mapping` lassen sich links in einer `.yaml`-Datei festlegen und dann als Markdown-Link verwenden. ```yaml @@ -16,4 +15,25 @@ bar: https://bar.com Auf [foo](@foo) sieht man einen Link zu [bar](@bar). ``` -Das Schlüsselwort muss mit `@` beginnen, damit es als Link erkannt wird. +Der MD-Link muss mit einem @ beginnen, damit es der Schlüssel im YAML gesucht wird. + +## Vorteile +Links sind an einem Ort und können leicht überprüft/aktualisiert werden, ggf. auch automatisch. + +## Installation + +```ts title="siteConfig.ts" +import linkMappingPlugin from '@ict/link-mapping'; + +const getSiteConfig: SiteConfigProvider = () => { + return { + /*...*/ + remarkPlugins: [ + /*...*/ + // highlight-next-line + [linkMappingPlugin, { mappingFilePath: './docs/link-mappings.yaml' }] + /*...*/ + ], + } +} +``` \ No newline at end of file diff --git a/siteConfig.ts b/siteConfig.ts index eeae2d98..70630185 100644 --- a/siteConfig.ts +++ b/siteConfig.ts @@ -7,7 +7,6 @@ import { recommendedBeforeDefaultRemarkPlugins, recommendedRemarkPlugins } from './src/siteConfig/markdownPluginConfigs'; -import linkMappingRemarkPlugin from './packages/ict/link-mapping'; import { devModeAccessLocalFS, taskStateOverview } from './src/siteConfig/navbarItems'; import { SiteConfigProvider } from './src/siteConfig/siteConfig'; import detailsPlugin from './src/plugins/remark-details/plugin'; @@ -22,7 +21,7 @@ const ADMONITION_CONFIG = { }; import dynamicRouter from './src/plugins/plugin-dynamic-routes'; import { type DirectusConfig } from '@ict/directus'; -import path from 'path'; +import linkMappingPlugin from '@ict/link-mapping'; declare module './src/siteConfig/siteConfig' { export interface TdevConfig { @@ -141,7 +140,6 @@ const getSiteConfig: SiteConfigProvider = () => { } ], beforeDefaultRemarkPlugins: [ - [linkMappingRemarkPlugin, { mappingFilePath: './docs/link-mappings.yaml' }], ...recommendedBeforeDefaultRemarkPlugins.filter((p) => p !== detailsPluginConfig), [ detailsPlugin, @@ -158,6 +156,7 @@ const getSiteConfig: SiteConfigProvider = () => { ] ] as unknown as PluginOptions[], remarkPlugins: [ + [linkMappingPlugin, { mappingFilePath: './docs/link-mappings.yaml' }], ...recommendedRemarkPlugins.filter((p) => p !== commentPluginConfig) ] as unknown as PluginOptions[], apiDocumentProviders: [