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..32ed675f --- /dev/null +++ b/packages/ict/link-mapping/README.mdx @@ -0,0 +1,39 @@ +--- +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). +``` + +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/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..70630185 100644 --- a/siteConfig.ts +++ b/siteConfig.ts @@ -21,6 +21,7 @@ const ADMONITION_CONFIG = { }; import dynamicRouter from './src/plugins/plugin-dynamic-routes'; import { type DirectusConfig } from '@ict/directus'; +import linkMappingPlugin from '@ict/link-mapping'; declare module './src/siteConfig/siteConfig' { export interface TdevConfig { @@ -154,9 +155,10 @@ const getSiteConfig: SiteConfigProvider = () => { } ] ] as unknown as PluginOptions[], - remarkPlugins: recommendedRemarkPlugins.filter( - (p) => p !== commentPluginConfig - ) as unknown as PluginOptions[], + remarkPlugins: [ + [linkMappingPlugin, { mappingFilePath: './docs/link-mappings.yaml' }], + ...recommendedRemarkPlugins.filter((p) => p !== commentPluginConfig) + ] as unknown as PluginOptions[], apiDocumentProviders: [ require.resolve('@tdev/page-read-check/register'), require.resolve('@ict/directus/register')