|
| 1 | +// @ts-check |
| 2 | + |
| 3 | +/** |
| 4 | + * Plugin to detect languages supported in tutorial pages by scanning for tab labels. |
| 5 | + * This creates shared data that maps tutorial paths to their supported languages. |
| 6 | + */ |
| 7 | +export function tutorialLanguages() { |
| 8 | + /** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */ |
| 9 | + const instance = { |
| 10 | + processContent: async (actions, { fs, cache }) => { |
| 11 | + try { |
| 12 | + /** @type {Record<string, string[]>} */ |
| 13 | + const tutorialLanguagesMap = {}; |
| 14 | + const allFiles = await fs.scan(); |
| 15 | + |
| 16 | + // Find all markdown files in tutorials directory |
| 17 | + const tutorialFiles = allFiles.filter((file) => |
| 18 | + file.relativePath.match(/^docs[\/\\]tutorials[\/\\].*\.md$/) |
| 19 | + ); |
| 20 | + |
| 21 | + for (const { relativePath } of tutorialFiles) { |
| 22 | + try { |
| 23 | + const { data } = await cache.load(relativePath, 'markdown-ast'); |
| 24 | + const languages = extractLanguagesFromAst(data.ast); |
| 25 | + |
| 26 | + if (languages.length > 0) { |
| 27 | + // Convert file path to URL path |
| 28 | + const urlPath = '/' + relativePath |
| 29 | + .replace(/\.md$/, '/') |
| 30 | + .replace(/\\/g, '/'); |
| 31 | + tutorialLanguagesMap[urlPath] = languages; |
| 32 | + } |
| 33 | + } catch (err) { |
| 34 | + continue; // Skip files that can't be parsed. |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + actions.createSharedData('tutorial-languages', tutorialLanguagesMap); |
| 39 | + actions.addRouteSharedData('/docs/tutorials/', 'tutorial-languages', 'tutorial-languages'); |
| 40 | + actions.addRouteSharedData('/ja/docs/tutorials/', 'tutorial-languages', 'tutorial-languages'); |
| 41 | + actions.addRouteSharedData('/es-es/docs/tutorials/', 'tutorial-languages', 'tutorial-languages'); |
| 42 | + } catch (e) { |
| 43 | + console.log('[tutorial-languages] Error:', e); |
| 44 | + } |
| 45 | + }, |
| 46 | + }; |
| 47 | + return instance; |
| 48 | +} |
| 49 | + |
| 50 | +/** |
| 51 | + * Extract language names from tab labels in the markdown AST |
| 52 | + */ |
| 53 | +function extractLanguagesFromAst(ast) { |
| 54 | + const languages = new Set(); |
| 55 | + |
| 56 | + visit(ast, (node) => { |
| 57 | + // Look for tab nodes with a label attribute |
| 58 | + if (isNode(node) && node.type === 'tag' && node.tag === 'tab') { |
| 59 | + const label = node.attributes?.label; |
| 60 | + if (label) { |
| 61 | + const normalizedLang = normalizeLanguage(label); |
| 62 | + if (normalizedLang) { |
| 63 | + languages.add(normalizedLang); |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | + }); |
| 68 | + |
| 69 | + return Array.from(languages); |
| 70 | +} |
| 71 | + |
| 72 | +/** |
| 73 | + * Convert tab labels like "JavaScript", "Python", etc. to lowercase keys |
| 74 | + * used for displaying the correct language icons on tutorial cards. |
| 75 | + */ |
| 76 | +function normalizeLanguage(label) { |
| 77 | + const labelLower = label.toLowerCase(); |
| 78 | + |
| 79 | + if (labelLower.includes('javascript') || labelLower === 'js') { |
| 80 | + return 'javascript'; |
| 81 | + } |
| 82 | + if (labelLower.includes('python') || labelLower === 'py') { |
| 83 | + return 'python'; |
| 84 | + } |
| 85 | + if (labelLower.includes('java') && !labelLower.includes('javascript')) { |
| 86 | + return 'java'; |
| 87 | + } |
| 88 | + if (labelLower.includes('php')) { |
| 89 | + return 'php'; |
| 90 | + } |
| 91 | + if (labelLower.includes('go') || labelLower === 'golang') { |
| 92 | + return 'go'; |
| 93 | + } |
| 94 | + if (labelLower.includes('http') || labelLower.includes('websocket')) { |
| 95 | + return 'http'; |
| 96 | + } |
| 97 | + |
| 98 | + return null; |
| 99 | +} |
| 100 | + |
| 101 | +function isNode(value) { |
| 102 | + return !!(value?.$$mdtype === 'Node'); |
| 103 | +} |
| 104 | + |
| 105 | +function visit(node, visitor) { |
| 106 | + if (!node) return; |
| 107 | + |
| 108 | + visitor(node); |
| 109 | + |
| 110 | + if (node.children) { |
| 111 | + for (const child of node.children) { |
| 112 | + if (!child || typeof child === 'string') { |
| 113 | + continue; |
| 114 | + } |
| 115 | + visit(child, visitor); |
| 116 | + } |
| 117 | + } |
| 118 | +} |
0 commit comments