跳转到内容

占位符系统

占位符系统允许读者在整个文档中自定义 IP 地址、ASN 及其他与部署相关的值。作者在 Markdown 中编写令牌;浏览器在运行时将其替换为用户提供的值。

令牌遵循以下正则表达式模式:

x([A-Z][A-Z0-9_]+)x

令牌以小写的 x 开头和结尾,中间包含一个大写标识符。例如,xCUSTOMER_ASNx 引用 CUSTOMER_ASN 占位符。

该正则表达式定义在 src/scripts/placeholder-dom.ts 中:

const PH_REGEX = /x([A-Z][A-Z0-9_]+)x/g;

所有占位符都声明在 src/data/placeholders.json 中。每个条目具有以下结构:

{
"CUSTOMER_ASN": {
"type": "text",
"default": "64496",
"description": "Your public ASN (registered with ARIN/RIR)"
}
}
字段是否必需描述
type"text" 表示自由输入,"dropdown" 表示下拉选择菜单
default读者更改之前显示的初始值
description表单中显示的标签
options仅下拉类型需要允许值的数组

src/lib/placeholder-store.ts 处理所有占位符状态。

值持久化在 localStorage 中,键名为 f5xc-placeholders。存储模块暴露四个函数:

函数用途
getDefaults()返回每个占位符键到其 JSON 中 default 值的映射
loadValues()从 localStorage 读取,失败时回退到 getDefaults()
saveValues(values)将当前映射写入 localStorage
clearValues()移除 localStorage 条目

FIELD_GROUPS 将占位符键组织成带标签的分组,用于表单 UI:

export const FIELD_GROUPS: FieldGroup[] = [
{ label: 'Data Center & Scrubbing Centers', keys: ['DC_NAME', 'CENTER_1', 'CENTER_2'] },
{ label: 'Protected Prefixes', keys: ['PROTECTED_CIDR_V4', 'PROTECTED_NET_V4', ...] },
{ label: 'BGP', keys: ['CUSTOMER_ASN', 'F5_XC_ASN', 'BGP_PASSWORD'] },
// ... 更多分组
];

某些值是从用户输入派生的,而非直接输入。getComputedValues() 通过查找表计算这些值:

const cidrToMask: Record<string, string> = {
'/24 (256 IPs)': '255.255.255.0',
'/23 (512 IPs)': '255.255.254.0',
// ...
};

产生两个计算占位符:

计算键派生来源示例
PROTECTED_MASK_V4PROTECTED_CIDR_V4 通过 cidrToMask 查找255.255.255.0
PROTECTED_PREFIX_V4PROTECTED_NET_V4 + PROTECTED_CIDR_V4 通过 cidrToShort192.0.2.0/24

getAllValues() 将用户输入的值与计算值合并,提供用于替换的完整映射。

emitChange()document 上派发一个 placeholder-change CustomEvent,将完整的值映射作为 detail

export function emitChange(values: Record<string, string>) {
document.dispatchEvent(
new CustomEvent('placeholder-change', { detail: getAllValues(values) }),
);
}

此事件驱动 DOM span 更新和 Mermaid 重新渲染。

src/components/PlaceholderForm.tsx 提供编辑界面。

  • 状态useStateloadValues() 初始化
  • 挂载时useEffect 调用 emitChange() 以触发初始 DOM 替换
  • handleChange:更新 React 状态,调用 saveValues()emitChange()
  • handleReset:调用 clearValues(),将状态重置为 getDefaults(),发送变更事件
  • 渲染:遍历 FIELD_GROUPS,为每个分组渲染一个 <fieldset>。每个键根据类型渲染 <input>(文本类型)或 <select>(下拉类型)
  • 布局:表单包裹在 <details> 元素中,默认折叠

src/components/PlaceholderFormWrapper.astro 将 React 组件连接到 Astro 页面:

<PlaceholderForm client:only="react" />
<script>
import '../scripts/placeholder-dom.ts';
</script>

client:only="react" 告知 Astro 仅在客户端水合组件(无 SSR)。<script> 标签导入 DOM 遍历器,使其在包含此包装器的每个页面上运行。

包装器还注入了用于表单样式的全局 CSS(.ph-form-wrapper.ph-grid.ph-value 等)。

src/scripts/placeholder-dom.ts 处理客户端的令牌替换。

页面加载时,init() 运行:

  1. 选择 .sl-markdown-content 作为根节点(回退到 document.body
  2. 调用 walkTextNodes(root, values),使用 document.createTreeWalker 并设置 NodeFilter.SHOW_TEXT
  3. 对于每个匹配令牌正则表达式的文本节点,将其拆分为由纯文本节点和 <span data-ph="KEY" class="ph-value"> 元素组成的文档片段
  4. 用该片段替换原始文本节点

遍历完成后,DOM 中包含带有 data-ph 属性的 span,而不是原始令牌。

当表单发出 placeholder-change 事件时,updateSpans() 运行:

document.querySelectorAll<HTMLSpanElement>('span[data-ph]').forEach((span) => {
const name = span.getAttribute('data-ph')!;
if (values[name] !== undefined) {
span.textContent = values[name];
}
});

这避免了重新遍历整棵树——它直接更新 span 的文本内容。

该脚本注册了两个监听器:

事件处理器用途
placeholder-changehandleChange更新 span 并重新渲染 Mermaid 图表
astro:page-loadinit在 Astro 客户端导航后重新遍历 DOM
  1. src/data/placeholders.json 中添加 JSON 条目

    "MY_NEW_VALUE": {
    "type": "text",
    "default": "example",
    "description": "Description shown in the form"
    }
  2. 将键添加到字段分组中,在 src/lib/placeholder-store.ts 中。将其添加到现有分组的 keys 数组中,或在 FIELD_GROUPS 中创建新分组。

  3. 在内容中使用令牌:在任何 .mdx 文件中写入 xMY_NEW_VALUEx。DOM 遍历器将在运行时替换它。

计算值是从其他占位符派生的。要添加计算值:

  1. src/lib/placeholder-store.ts 中添加查找表(如需要),参照 cidrToMask 的模式。

  2. 扩展 getComputedValues() 以包含新的派生键:

    export function getComputedValues(values: Record<string, string>): Record<string, string> {
    // ... 现有逻辑
    return {
    PROTECTED_MASK_V4: mask,
    PROTECTED_PREFIX_V4: `${net}${short}`,
    MY_COMPUTED: derivedValue, // 在此添加
    };
    }
  3. 像使用其他令牌一样在内容中使用 xMY_COMPUTEDx。计算值不需要 placeholders.json 条目或字段分组——它们对表单不可见。