(href: string)
| 158 | * RelationType,由 WikiLink 组件渲染徽章。 |
| 159 | */ |
| 160 | export function parseKbLink(href: string): { |
| 161 | target: string; |
| 162 | anchor: string | null; |
| 163 | relation: RelationType | null; |
| 164 | } | null { |
| 165 | if (!isKbLink(href)) return null; |
| 166 | const stripped = href.slice(KB_LINK_PREFIX.length); |
| 167 | // 解析顺序:hash 永远在 query 之后(preprocessWikiLinks 也是这么写入的), |
| 168 | // 所以先按 # 切,再在前半段按 ? 切 |
| 169 | const hashIdx = stripped.indexOf("#"); |
| 170 | const beforeHash = hashIdx === -1 ? stripped : stripped.slice(0, hashIdx); |
| 171 | let anchor: string | null = null; |
| 172 | if (hashIdx !== -1) { |
| 173 | const rawAnchor = stripped.slice(hashIdx + 1); |
| 174 | try { |
| 175 | anchor = decodeURIComponent(rawAnchor); |
| 176 | } catch { |
| 177 | anchor = rawAnchor; |
| 178 | } |
| 179 | } |
| 180 | const queryIdx = beforeHash.indexOf("?"); |
| 181 | const rawTarget = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx); |
| 182 | let target: string; |
| 183 | try { |
| 184 | target = decodeURIComponent(rawTarget); |
| 185 | } catch { |
| 186 | target = rawTarget; |
| 187 | } |
| 188 | let relation: RelationType | null = null; |
| 189 | if (queryIdx !== -1) { |
| 190 | const queryStr = beforeHash.slice(queryIdx + 1); |
| 191 | const params = new URLSearchParams(queryStr); |
| 192 | const rel = params.get("rel"); |
| 193 | // 只认白名单内的关系类型,与 splitAliasOrRelation 口径一致—— |
| 194 | // 防止伪造 / 拼错的 ?rel= 值被当成合法 RelationType 透传给徽章渲染。 |
| 195 | if (rel && RELATION_TYPES.has(rel)) relation = rel as RelationType; |
| 196 | } |
| 197 | return { target, anchor, relation }; |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * 识别 convert.py 自动加在 heading / paragraph / table / code / figure 末尾的锚点: |
no test coverage detected