* Copyright (c) 2026 Huawei Technologies Co., Ltd.
* This program is free software, you can redistribute it and/or modify it under the terms and conditions of
* CANN Open Software License Agreement Version 2.0 (the "License").
* Please refer to the License for details. You may not use this file except in compliance with the License.
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
* See LICENSE in the root of the software repository for the full text of the License.
*/
import { existsSync, statSync } from 'node:fs'
import { dirname, extname, isAbsolute, relative, resolve } from 'node:path'
const REPOSITORY_URL = 'https://gitcode.com/cann/asc-devkit'
const HREF_RE = /(\bhref\s*=\s*)(["'])([^"']*)\2/gi
const IMAGE_EXTENSIONS = new Set([
'.bmp', '.gif', '.ico', '.jpeg', '.jpg', '.png', '.svg', '.webp',
])
function isInside(parent, target) {
const pathFromParent = relative(parent, target)
return pathFromParent === '' || (!pathFromParent.startsWith('..') && !isAbsolute(pathFromParent))
}
function splitHref(href) {
const queryIndex = href.indexOf('?')
const hashIndex = href.indexOf('#')
const suffixIndex = [queryIndex, hashIndex]
.filter(index => index >= 0)
.reduce((first, index) => Math.min(first, index), href.length)
return [href.slice(0, suffixIndex), href.slice(suffixIndex)]
}
function encodeRepositoryPath(path) {
return path
.replace(/\\/g, '/')
.split('/')
.filter(Boolean)
.map(segment => encodeURIComponent(segment))
.join('/')
}
export function rewriteRepositoryLinks(html, sourceFile, repoRoot) {
if (!sourceFile || !repoRoot) return html
const codeBlocks = []
html = html.replace(/<(code|pre)\b[^>]*>[\s\S]*?<\/\1>/gi, (m) => {
codeBlocks.push(m)
return `\x00CB${codeBlocks.length - 1}\x00`
})
html = html.replace(HREF_RE, (original, attribute, quote, href) => {
const rewrittenHref = rewriteRepositoryHref(href, sourceFile, repoRoot)
if (rewrittenHref === href) return original
return `${attribute}${quote}${rewrittenHref}${quote}`
})
html = html.replace(/\x00CB(\d+)\x00/g, (_, i) => codeBlocks[+i])
return html
}
export function rewriteRepositoryHref(href, sourceFile, repoRoot) {
if (!sourceFile || !repoRoot) return href
const normalizedRepoRoot = resolve(repoRoot)
const docsRoot = resolve(normalizedRepoRoot, 'docs')
if (
!href ||
href.startsWith('/') ||
href.startsWith('#') ||
/^[a-z][a-z0-9+.-]*:/i.test(href)
) {
return href
}
const [pathPart, suffix] = splitHref(href)
let target
try {
target = resolve(dirname(sourceFile), decodeURI(pathPart))
} catch {
return href
}
if (!isInside(normalizedRepoRoot, target) || isInside(docsRoot, target)) {
return href
}
if (!existsSync(target) || IMAGE_EXTENSIONS.has(extname(target).toLowerCase())) {
return href
}
let linkType
try {
linkType = statSync(target).isDirectory() ? 'tree' : 'blob'
} catch {
return href
}
const repositoryPath = encodeRepositoryPath(relative(normalizedRepoRoot, target))
const separator = repositoryPath ? '/' : ''
return `${REPOSITORY_URL}/${linkType}/master${separator}${repositoryPath}${suffix}`
}
export function installRepositoryLinkRewrite(md, getSourceFile, repoRoot) {
const originalLinkOpen = md.renderer.rules.link_open
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const hrefIndex = token.attrIndex('href')
if (hrefIndex >= 0) {
const href = token.attrs[hrefIndex][1]
token.attrs[hrefIndex][1] = rewriteRepositoryHref(href, getSourceFile(env), repoRoot)
}
if (originalLinkOpen) {
return originalLinkOpen(tokens, idx, options, env, self)
}
return self.renderToken(tokens, idx, options)
}
}