| | import { readFile } from 'fs/promises' |
| | import path from 'path' |
| |
|
| | import { fromMarkdown } from 'mdast-util-from-markdown' |
| | import { toMarkdown } from 'mdast-util-to-markdown' |
| | import { visitParents } from 'unist-util-visit-parents' |
| | import { visit, SKIP } from 'unist-util-visit' |
| | import { remove } from 'unist-util-remove' |
| |
|
| | import { languageKeys } from '@/languages/lib/languages-server' |
| | import { MARKDOWN_OPTIONS } from '../../content-linter/lib/helpers/unified-formatter-options' |
| |
|
| | interface Config { |
| | targetDirectory: string |
| | removeKeywords: string[] |
| | } |
| |
|
| | interface FrontmatterDefaults { |
| | [key: string]: string |
| | } |
| |
|
| | interface Frontmatter { |
| | title: string |
| | intro?: string |
| | [key: string]: string | undefined |
| | } |
| |
|
| | interface ConversionResult { |
| | content: string |
| | data: Frontmatter |
| | } |
| |
|
| | const config: Config = JSON.parse( |
| | await readFile(path.join('src/codeql-cli/lib/config.json'), 'utf-8'), |
| | ) |
| | const { targetDirectory, removeKeywords } = config |
| | const RELATIVE_LINK_PATH = targetDirectory.replace('content', '') |
| | const LAST_PRIMARY_HEADING = 'Primary options' |
| | const HEADING_BEGIN = '::: {.option}\n' |
| | const END_SECTION = '\n:::' |
| | const PROGRAM_SECTION = '::: {.program}\n' |
| |
|
| | |
| | export async function convertContentToDocs( |
| | content: string, |
| | frontmatterDefaults: FrontmatterDefaults = {}, |
| | currentFileName = '', |
| | ): Promise<ConversionResult> { |
| | const ast = fromMarkdown(content) |
| |
|
| | let depth = 0 |
| | let secondaryOptions = false |
| | const frontmatter: Frontmatter = { title: '', ...frontmatterDefaults } |
| | const akaMsLinkMatches: any[] = [] |
| |
|
| | |
| | visit(ast, 'heading', (node: any) => { |
| | |
| | |
| | if (node.depth === 1) { |
| | frontmatter.title = node.children[0].value |
| | } |
| |
|
| | |
| | |
| | |
| | if (node.children[0].value.includes('{#')) { |
| | node.children[0].value = node.children[0].value.split('{#')[0].trim() |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if (secondaryOptions) { |
| | node.depth = Math.max(1, Math.min(6, node.depth - 1)) |
| | } |
| |
|
| | |
| | depth = node.depth |
| | if (node.children[0].value === LAST_PRIMARY_HEADING && node.children[0].type === 'text') { |
| | secondaryOptions = true |
| | } |
| | }) |
| |
|
| | |
| | let currentNodeIsDescription = false |
| | visit(ast, (node: any) => { |
| | if (node.type !== 'heading' && node.type !== 'paragraph') return false |
| |
|
| | |
| | |
| | |
| | if (node.children[0]?.value === 'Description' && node.children[0]?.type === 'text') { |
| | currentNodeIsDescription = true |
| | } |
| | if (currentNodeIsDescription && node.type === 'paragraph') { |
| | frontmatter.intro = node.children[0]?.value |
| | currentNodeIsDescription = false |
| | return SKIP |
| | } |
| | }) |
| |
|
| | |
| | const matchNodeTypes = ['text', 'code', 'link'] |
| | visitParents( |
| | ast, |
| | (node: any) => { |
| | return node && matchNodeTypes.includes(node.type) |
| | }, |
| | (node: any, ancestors: any[]) => { |
| | |
| | if (node.type === 'code' && node.value.startsWith(`codeql ${frontmatter.title}`)) { |
| | node.lang = 'shell' |
| | node.meta = 'copy' |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if (node.type === 'text' && node.value && node.value.includes(HEADING_BEGIN)) { |
| | node.value = node.value.replace(HEADING_BEGIN, '') |
| | |
| | |
| | ancestors[ancestors.length - 1].type = 'heading' |
| | ancestors[ancestors.length - 1].depth = Math.max(1, Math.min(6, depth + 1)) |
| | } |
| |
|
| | |
| | |
| | if (node.type === 'text' && node.value) { |
| | for (const keyword of removeKeywords) { |
| | if (node.value.includes(keyword)) { |
| | node.value = node.value.replace(keyword, '').trim() |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | if ( |
| | node.type === 'text' && |
| | ancestors[ancestors.length - 1].type === 'heading' && |
| | (node.value.startsWith('-') || node.value.startsWith('<')) |
| | ) { |
| | node.type = 'inlineCode' |
| | } |
| |
|
| | |
| | |
| | if (node.type === 'text' && node.value && node.value.includes(END_SECTION)) { |
| | node.value = node.value.replace(END_SECTION, '') |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (node.type === 'text' && node.value.includes('{.interpreted-text')) { |
| | const paragraph = ancestors[ancestors.length - 1].children |
| | const docRoleTagChild = paragraph.findIndex( |
| | (child: any) => child.value && child.value.includes('{.interpreted-text'), |
| | ) |
| | const link = paragraph[docRoleTagChild - 1] |
| | |
| | if (link.type === 'link') { |
| | return |
| | } |
| | |
| | |
| | |
| | if (link.type !== 'inlineCode') { |
| | throw new Error( |
| | 'Unexpected node type. The node before a text node with {.interpreted-text role="doc"} should be an inline code or link node.', |
| | ) |
| | } |
| |
|
| | |
| | |
| | const linkText = link.value.split('<')[0].replace(/\n/g, ' ').trim() |
| | const linkPath = link.value.split('<')[1].split('>')[0].replace(/'\n/g, '').trim() |
| |
|
| | |
| | node.value = node.value.replace(/\n/g, ' ').replace('{.interpreted-text role="doc"}', '') |
| |
|
| | |
| | const currentFileBaseName = currentFileName.replace('.md', '') |
| | if (currentFileBaseName && linkPath === currentFileBaseName) { |
| | |
| | link.type = 'text' |
| | link.value = linkText |
| | } else { |
| | |
| | link.type = 'link' |
| | link.url = `${RELATIVE_LINK_PATH}/${linkPath}` |
| | link.children = [{ type: 'text', value: linkText }] |
| | delete link.value |
| | } |
| | } |
| |
|
| | |
| | if (node.type === 'link' && node.url.includes('aka.ms')) { |
| | akaMsLinkMatches.push(node) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if (node.type === 'link' && node.url.startsWith('https://containers')) { |
| | |
| | const nodeBefore = ancestors[ancestors.length - 1].children[0] |
| | const nodeAfter = ancestors[ancestors.length - 1].children[2] |
| | if (nodeBefore.value && nodeBefore.value.endsWith('"')) { |
| | nodeBefore.value = nodeBefore.value.slice(0, -1) |
| | } |
| | if (nodeAfter.value && nodeAfter.value.startsWith('"')) { |
| | nodeAfter.value = nodeAfter.value.slice(1) |
| | } |
| | |
| | node.type = 'inlineCode' |
| | node.value = node.url |
| | node.title = undefined |
| | node.url = undefined |
| | node.children = undefined |
| | } |
| | }, |
| | ) |
| |
|
| | |
| | await Promise.all( |
| | akaMsLinkMatches.map(async (node: any) => { |
| | const url = await getRedirect(node.url) |
| | |
| | |
| | |
| | if (node.children[0]) { |
| | node.children[0].value = 'AUTOTITLE' |
| | } |
| | node.url = url |
| | }), |
| | ) |
| |
|
| | |
| | remove(ast, (node: any) => node.value && node.value.startsWith(PROGRAM_SECTION)) |
| | |
| | remove(ast, (node: any) => node.type === 'heading' && node.depth === 1) |
| |
|
| | return { content: toMarkdown(ast, MARKDOWN_OPTIONS as any), data: frontmatter } |
| | } |
| |
|
| | |
| | async function getRedirect(url: string): Promise<string> { |
| | let response: Response |
| | try { |
| | response = await fetch(url, { redirect: 'manual' }) |
| | if (!response.ok && response.status !== 301 && response.status !== 302) { |
| | throw new Error(`HTTP ${response.status}: ${response.statusText}`) |
| | } |
| | } catch (error) { |
| | console.error(error) |
| | const errorMsg = `Failed to get redirect for ${url} when converting aka.ms links to docs.github.com links.` |
| | throw new Error(errorMsg) |
| | } |
| |
|
| | |
| | const redirectLocation = response.headers.get('location') |
| | if (!redirectLocation) { |
| | throw new Error(`No redirect location found for ${url}`) |
| | } |
| |
|
| | |
| | const redirect = new URL(redirectLocation).pathname |
| |
|
| | |
| | |
| | const redirectNoLang = languageKeys.reduce((acc, lang) => { |
| | return acc.replace(`/${lang}`, ``) |
| | }, redirect) |
| |
|
| | if (!redirectNoLang) { |
| | const errorMsg = `The aka.ms redirected to an unexpected url: ${url}` |
| | throw new Error(errorMsg) |
| | } |
| |
|
| | return redirectNoLang |
| | } |
| |
|