Universal Ctags 的 R 语言解析器详解:Kinds、构造函数推断与标签生成实战
开发工具CLI【免费下载链接】ctagsA maintained ctags implementation项目地址https://gitcode.com/gh_mirrors/ct/ctags点击查看免费下载导读本文基于 Universal Ctags 仓库中 docs/man/ctags-lang-r.7.rst 手册页系统讲解如何用 ctags 为 R 统计计算语言生成标签tag。你将掌握 R 解析器内置的 10 种标签 kinds、四种知名构造函数function()、c()、list()、data.frame()到 kind 的自动映射规则、globalVar/functionVar的上下文判别逻辑以及 scope 作用域字段在嵌套函数与数据结构中的行为。文中还会带你对照 parsers/r.c 源码与 Units/parser-r.r 测试用例理解这些规则背后的实现原理从而能够精准定制自己的 ctags 命令行。一、在命令行中启用 R 解析器R 解析器是 Universal Ctags 内置的语言解析器之一对应源码文件 parsers/r.c语言名R。手册给出的三种典型调用方式如下ctags ... --languagesR ... ctags ... --language-forceR ... ctags ... --map-R.r ...三者的适用场景各不相同选项作用--languagesR在默认语言集合之外追加启用R 解析器让 ctags 按扩展名自动识别.r文件--language-forceR强制把后续所有输入文件都当作 R 语言解析忽略扩展名猜测例如处理无扩展名或扩展名奇特的文件--map-R.r为 R 语言追加文件扩展名映射使其识别.r等扩展名从 parsers/r.c 第 1391 行的RParser()定义可见R 解析器声明的默认扩展名列表为static const char *const extensions[] { r, R, s, q, NULL };即.r、.R、.s、.q四种扩展名默认即与 R 语言关联并且该解析器还注册了一个selectByArrowOfR语言选择器第 13931394 行用于在没有扩展名提示时按内容特征猜测语言。需要注意的是手册中的 SYNOPSIS 均使用...省略号表示其余通用选项完整选项列表请参阅 docs/man/ctags.1.rst 中对应选项的说明。二、核心机制知名构造函数与 Kind 的映射这是 R 解析器最突出的设计当一个变量首次在当前输入文件中出现并且其赋值右侧来自知名构造函数well-known constructor的返回值时解析器就会为这个变量生成标签并挂上与该构造函数对应的 kind——无论该变量出现在顶层上下文还是某个函数体内。手册给出了明确的映射表构造函数kindfunction()functionc()vectorlist()listdata.frame()dataframe源码层面这个映射由 parsers/r.c 中的sKindExtraInfo结构体实现第 120142 行struct sKindExtraInfo { const char *anon_prefix; const char *ctor; }; static struct sKindExtraInfo kindExtraInfo[KIND_COUNT] { [K_FUNCTION] { anonFunc, function, }, [K_VECTOR] { anonVec, c, }, [K_LIST] { anonList, list, }, [K_DATAFRAME] { anonDataFrame, data.frame, }, };其中ctor字段正是知名构造函数的名字anon_prefix则是后面会讲到的匿名标签前缀。而解析时判断 kind 的逻辑集中在getKindForToken()第 863874 行static int getKindForToken (tokenInfo *const token) { if (tokenIsKeyword (token, R_FUNCTION)) return K_FUNCTION; else if (tokenIsKeyword (token, R_C)) return K_VECTOR; else if (tokenIsKeyword (token, R_LIST)) return K_LIST; else if (tokenIsKeyword (token, R_DATAFRAME)) return K_DATAFRAME; return K_GLOBALVAR; }即读取赋值运算符右侧的第一个 token若命中function/c/list/data.frame这四个关键字则赋予对应 kind否则回退为K_GLOBALVAR。这四个关键字的识别来自 parsers/r.c 第 164190 行的关键字表RKeywordTable其中甚至包含library/require两者统一映射为KEYWORD_R_LIBRARY与source关键字用于后文提到的外部实体引用标签。三、R 解析器的完整 Kinds 一览虽然手册正文只重点讲述了上述构造函数映射但从源码的RKinds表parsers/r.c 第 105118 行可以确认 R 解析器一共定义了 10 种 kind这也是手册中 TODO 标注other kinds所指的内容字母长名说明默认启用ffunction函数是llibrary库仅引用referenceOnly是ssource源文件仅引用referenceOnly是gglobalVar值为非函数对象的全局变量是vfunctionVar函数体内的非函数变量是zparameter函数定义内的形参否默认关闭cvector用c()显式创建的向量是Llist用list()显式创建的列表是ddataframe用data.frame()显式创建的数据框是nnameattr向量、列表或数据框中的命名属性names 属性是其中library与source是referenceOnly类型并且各自带有角色rolelibrary区分由library()附加与由require()附加两种角色第 9699 行source只有由source()加载一种角色第 101103 行。parameterkind 默认关闭可通过--kinds-Rz之类的方式启用。static kindDefinition RKinds[KIND_COUNT] { {true, f, function, functions}, {true, l, library, libraries, .referenceOnly true, ATTACH_ROLES (RLibraryRoles) }, {true, s, source, sources, .referenceOnly true, ATTACH_ROLES (RSourceRoles) }, {true, g, globalVar, global variables having values other than function()}, {true, v, functionVar, function variables having values other than function()}, {false,z, parameter, function parameters inside function definitions }, {true, c, vector, vectors explicitly created with c() }, {true, L, list, lists explicitly created with list() }, {true, d, dataframe, data frame explicitly created with data.frame() }, {true, n, nameattr, names attributes in vectors, lists, or dataframes }, };在R语言上下文之外library(...)/require(...)/source(...)调用会为被加载的模块名生成library或source引用标签实现在preParseExternalEntitiy()第 9721029 行并通过makeSimpleRefTag携带对应角色。四、手动示例input.r 的标签输出全解手册用一个完整的例子演示上述 kinds 的用法。输入文件input.r内容如下G - 1 v - c(1, 2) l - list(3, 4) d - data.frame(n v) f - function(a) { g - function (b) a b w - c(1, 2) m - list (3, 4) e - data.frame(n w) L - 2 }用以下命令生成标签注意--optionsNONE表示不加载任何配置文件保证输出纯净可复现ctags --optionsNONE --sortno --fieldsKZ -o - input.r得到output.tagsG input.r /^G - 1$/; globalVar v input.r /^v - c(1, 2)$/; vector l input.r /^l - list(3, 4)$/; list d input.r /^d - data.frame(n v)$/; dataframe n input.r /^d - data.frame(n v)$/; nameattr scope:dataframe:d f input.r /^f - function(a) {$/; function g input.r /^ g - function (b) a b$/; function scope:function:f w input.r /^ w - c(1, 2)$/; vector scope:function:f m input.r /^ m - list (3, 4)$/; list scope:function:f e input.r /^ e - data.frame(n w)$/; dataframe scope:function:f n input.r /^ e - data.frame(n w)$/; nameattr scope:dataframe:f.e L input.r /^ L - 2$/; functionVar scope:function:f逐条解读这份输出你可以清楚地看到整套规则在实战中的表现全局赋值G - 1右侧是字面量1不属于任何知名构造函数因此回退为globalVar。v - c(1, 2)、l - list(3, 4)、d - data.frame(n v)分别命中c()/list()/data.frame()构造函数得到vector/list/dataframekind。n的两条nameattr标签data.frame(n v)与e - data.frame(n w)中data.frame(...)调用括号内的命名实参n ...被识别为数据框的命名属性names 属性生成nameattrkind 标签并带有scope:dataframe:d/scope:dataframe:f.e作用域——注意第二个作用域使用了点分层级f.e函数f内的数据框e。f - function(a)与嵌套g - function (b)函数定义得到functionkind嵌套函数g的作用域是scope:function:f。w、m、e虽然出现在函数f体内但由于右侧是知名构造函数依然获得vector/list/dataframekind并记录scope:function:f——这正印证了手册强调的无论顶层还是函数内规则。L - 2位于函数体内的普通赋值右侧是字面量因此在函数上下文中回退为functionVar并带scope:function:f。字段K与Z命令中的--fieldsKZ会额外输出 kind 长名K与语言信息Z所以每行末尾的globalVar、vector等都是长名输出而scope:前缀字段则由--fields的默认值提供。关于globalVar与functionVar的判别逻辑源码中makeSimpleRTagR()parsers/r.c 第 309378 行是关键当标签所在作用域parent是一个function类标签时K_GLOBALVAR会被改写为K_FUNCVAR第 331337 行同时该函数还会处理-/-全局赋值运算符与同作用域同名标签的去重第 312324、326359 行避免在同一作用域内为同名变量重复出标签。五、作用域scope机制为什么 nameattr 会挂到 dataframe 上上面的输出中最值得深入的是nameattr标签及其作用域。从 parsers/r.c 的makeSimpleRTag()第 380418 行可以看到static int makeSimpleRTag (tokenInfo *const token, int parent, bool in_func, int kind, const char * assignmentOp) { ... const char *ctor kindExtraInfo [kind].ctor; tagEntryInfo *pe (parent CORK_NIL)? NULL: getEntryInCorkQueue (parent); /* makeTagWithTranslation method for subparsers called from makeSimpleSubparserTag expects kind should be resolved. */ if (pe hasKindsOrCtors (pe, (int[]){K_VECTOR, K_LIST, K_DATAFRAME}, 3)) { if (assignmentOp strcmp (assignmentOp, ) 0) kind K_NAMEATTR; } ... if ((kind K_NAMEATTR || foreign_tag) ctor) { tagEntryInfo *e getEntryInCorkQueue (r); if (e) attachParserField (e, RFields [F_CONSTRUCTOR].ftype, ctor); } ... }核心逻辑是当一个赋值发生在向量、列表或数据框的构造参数列表内部且赋值运算符是时左值会被判定为命名属性kind 改写为K_NAMEATTR。这就是d - data.frame(n v)中n被标记为nameattr的根源。同时若该标签携带构造函数信息ctor非空还会额外附加constructor字段。此外data.frame(n1, 1:10, ...)这类没有左值变量的构造函数调用会生成匿名标签。测试用例 Units/parser-r.r/r-dataframe.d/input.r 与对应的 expected.tags 展示了这一行为anonDataFrame083788b40108 input.r /^data.frame(n1, 1:10, sample(L3, 10, replace TRUE))$/; d n input.r /^data.frame(n1, 1:10, sample(L3, 10, replace TRUE))$/; n dataframe:anonDataFrame083788b40108匿名标签的名字由kindExtraInfo中的anon_prefix如anonDataFrame、anonVec、anonList、anonFunc加哈希后缀构成匿名对象的机制由anonGenerate()完成。向量场景的同类行为见 Units/parser-r.r/r-vector.d/expected.tags 中的anonVec1725f6020206。六、字段与子解析器从手册 TODO 看扩展方向手册末尾的 TODO 区块列出了尚未写入手册正文、但已经在源码中实现的能力阅读源码可以确认它们大多已经落地其他 kinds即上文第 2 节列全的 10 种 kindlibrary/source/parameter/nameattr等完整定义见 parsers/r.c 的RKinds表。赋值运算符-、-、-、-、词法层面readToken()第 535813 行会识别左赋值TOKEN_R_LASSIGN-、-与右赋值TOKEN_R_RASSIGN-、-makeSimpleRTagR()中还会对长度为 3 的赋值运算符-、-做全局赋值语义处理。字段constructor与assignmentop这两个字段已在源码中实现parsers/r.c 第 144160 行static fieldDefinition RFields [] { { .name assignmentop, .description operator for assignment, .enabled false, }, { .name constructor, .description function used for making value assigned to the nameattr tag, .enabled true, } };assignmentop字段默认关闭可在命令行用--fields-R{assignmentop}打开用于输出赋值运算符constructor字段默认开启用于在nameattr标签上标注构造它的函数名。向量测试 Units/parser-r.r/r-vector.d/expected.tags 中可以看到实际输出例如constructor:function出现在nameattr标签行。子解析器sub parsersR 解析器实现了rSubparser接口定义在 parsers/x-r.h 第 7291 行包含readRightSideSymbol、makeTagWithTranslation、askTagAcceptancy、hasFunctionAlikeKind、readFuncall五个回调点。仓库中基于它构建了 R 的衍生语言解析器例如 parsers/r-r6class.cR6 面向对象类与 parsers/r-s4class.cS4 类分别对应测试目录 Units/parser-r.r 之外的parser-r6class.r、parser-s4class.r用例。七、实战验证用仓库测试用例核对行为仓库的 Units/parser-r.r 测试目录系统地覆盖了 R 解析器的各种行为是核对本文所述规则的最佳标本。例如r-simple.d/input.r 与 r-simple.d/expected.tags验证最基本的globalVarg、functionf与函数内functionVarv带function:foo作用域三种 kind与本文第 4 节示例的行为完全一致。r-dataframe.d/input.r 与 expected.tags验证data.frame()的dataframekind、命名实参的nameattrkindscope:dataframe:d、匿名数据框anonDataFrame...以及LETTERS[1:3]、sample(...)等普通赋值回退为globalVar的行为。r-vector.d/input.r 与 expected.tags验证c()内部的x function() 1这类值本身就是函数的命名属性会额外携带constructor:function字段z c(w - 1, b 1)中w - 1使用左赋值而非因此w不会被当作nameattr记录。其余用例如r-scope.d作用域、r-signature.d函数签名与参数、r-upper-scope-assignement.d-/-全局赋值、r-external-entities.dlibrary()/source()引用、r-dots.d.../..1记号分别覆盖了更多边界行为。运行这些测试的方式与 Universal Ctags 的单元测试体系一致参照 docs/testing-ctags.rst 使用make units即可批量执行misc/units脚本会为每个.d目录编译输入并逐一对比expected.tags。八、总结Universal Ctags 的 R 解析器把 R 语言的赋值表达式结构左值、赋值运算符、右侧构造调用直接映射为结构化的标签体系知名构造函数决定 kindfunction/vector/list/dataframe普通赋值按上下文落入globalVar/functionVar构造参数内的命名实参生成挂载在数据结构作用域下的nameattr配合constructor/assignmentop字段与 R6、S4 子解析器形成了对 R 代码相当完整的索引能力。结合本文的命令行示例与 parsers/r.c 源码、Units/parser-r.r 测试用例你可以放心地把它集成进自己的 R 项目标签生成流程。参见主手册ctags(1)R 解析器源码parsers/r.c、子解析器接口 parsers/x-r.h衍生解析器parsers/r-r6class.c、parsers/r-s4class.c测试用例Units/parser-r.r单元测试运行说明docs/testing-ctags.rst赞分享开发工具CLI【免费下载链接】ctagsA maintained ctags implementation项目地址https://gitcode.com/gh_mirrors/ct/ctags点击查看免费下载相关推荐Universal Ctags 的 CUDA 解析器语言启用、标签生成与字段机制详解Universal Ctags 的 CUDA 解析器语言启用、标签生成与字段机制详解 导读 CUDACompute Unified Device Archi开发工具CLIUniversal Ctags 的 R Markdown 解析器代码块Code Chunk的标签提取与 Guest Parser 集成实战Universal Ctags 的 R Markdown 解析器代码块Code Chunk的标签提取与 Guest Parser 集成实战 本篇技术指南围开发工具CLIes-toolkit compat 模块 slice 函数详解Lodash 兼容的数组切片实现与源码解析es toolkit compat 模块 slice 函数详解Lodash 兼容的数组切片实现与源码解析 本文围绕 es toolkit 的 Lodash 兼开发工具CLI上一篇CocoIndex 快速入门10分钟从零构建第一个向量索引下一篇Windows Cleaner5分钟掌握高效系统清理与优化技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

chan.py 缠论量化框架快速上手全指南:从 K 线计算到策略回测与机器学习接入

chan.py 缠论量化框架快速上手全指南:从 K 线计算到策略回测与机器学习接入

金融科技数据分析数据可视化 【免费下载链接】chan.py 开放式的缠论python实现框架,支持形态学/动力学买卖点分析计算,多级别K线联立,区间套策略,可视化绘图,多种数据接入,策略开发,交易系统对接…

2026/10/4 13:57:06 阅读更多 →
物联网操作系统选型指南:FreeRTOS、TinyOS与RT-Thread对比

物联网操作系统选型指南:FreeRTOS、TinyOS与RT-Thread对比

1. 物联网操作系统到底在解决什么问题我第一次接触物联网操作系统是在一个智能农业项目上,当时用裸机跑STM32,轮询加中断勉强撑住了十几个传感器。后来设备数量翻了三倍,还要加上无线通信协议栈和OTA升级,代码直接变成了一锅粥&am…

2026/10/5 15:46:14 阅读更多 →
Android Things 智能家居网关实战:架构、外设与规则引擎

Android Things 智能家居网关实战:架构、外设与规则引擎

1. 为什么 Android Things 做智能家居是个"看起来很美"的选择2016 年前后,智能家居赛道涌进来一大批开发者,手里攥着树莓派、各种开发板,脑子里想的都是"我要做一个自己的中控"。当时摆在面前的路无非几条:要…

2026/10/4 13:57:06 阅读更多 →

最新新闻

Python实时2D流体模拟:从Navier-Stokes到GPU加速的完整实现

Python实时2D流体模拟:从Navier-Stokes到GPU加速的完整实现

1. 这个项目到底想解决什么问题 先说结论:我这次折腾的目标,是纯粹用 Python 从零实现一个能实时交互的 2D 流体模拟器,核心基于 Navier-Stokes 方程,最终通过 GPU 加速把计算规模拉到肉眼可见的流畅帧率,整套流程覆盖…

2026/10/5 15:47:17 阅读更多 →
追觅AURORA剑指高端:扫地机器人的体验与算法才是“主考官”

追觅AURORA剑指高端:扫地机器人的体验与算法才是“主考官”

1. 高端家电这场“科举”,到底在考什么 1.1 参数战争早就出局,体验才是真正的“主考官” 国内清洁电器市场这几年卷到什么程度?吸尘器从几千帕一路卷到上万帕,扫地机器人从单机智能卷到全能基站,洗地机又沿着“洗得干…

2026/10/5 15:47:17 阅读更多 →
Open Shell 配置指南:在 Win10/11 中重造高效经典开始菜单

Open Shell 配置指南:在 Win10/11 中重造高效经典开始菜单

用 Open Shell 之前,我先说我为什么还在折腾开始菜单。Windows 10 出来的那阵子,我是第一波从 7 升上来的用户,升级完第一件事,不是去摸新功能,而是花二十分钟把系统自带的开始菜单点了个遍,然后得出结论&a…

2026/10/5 15:47:17 阅读更多 →
插件加载失败排查指南:从failed to load plugins到web boot

插件加载失败排查指南:从failed to load plugins到web boot

早上打开流水线控制台,一整片红色日志挂在屏幕中央,最扎眼的是这行: failed to load plugins ,后面跟着 web boot: 2 entries did not activate 。我这一年多里见过不少类似场面,在 Harness 的自托管代理上、在 IA…

2026/10/5 15:47:17 阅读更多 →
CloudCompare三维模型转点云全流程:采样、导出与工程实践指南

CloudCompare三维模型转点云全流程:采样、导出与工程实践指南

1. 为什么一直用CloudCompare做点云转换1.1 它和市面其他点云工具的区别做三维相关工作久了,你一定遇到过这种场景:甲方或同事甩过来一个OBJ、FBX或者SKP模型,说“帮我转成点云”或者“帮我把点放到模型表面”。以前遇到这种需求,…

2026/10/5 15:47:17 阅读更多 →
Superpowers技能框架实战:从零构建可组合的AI工作流

Superpowers技能框架实战:从零构建可组合的AI工作流

1. 从“superpowers”这个标题说起:它到底是什么 第一次看到“superpowers”这个词,很多人脑子里蹦出来的可能是超级英雄电影,或者某个游戏里的技能系统。但如果你最近在开发者社区、效率工具圈或者AI工作流相关的讨论里频繁刷到它&#xff0…

2026/10/5 15:46:17 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 5:06:42 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 3:06:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 20:14:29 阅读更多 →