Go 语言 JSON Schema 校验库 jsonschema/v6 完全指南:从库特性、Compiler API 到 jv 命令行工具
CLI开发工具【免费下载链接】cliThe Docker CLI项目地址https://gitcode.com/gh_mirrors/cli5/cli点击查看免费下载导读本文围绕 Docker CLI 仓库中引入的第三方 Go 依赖 santhosh-tekuri/jsonschema v6 展开这是一款以“先编译、后校验”为核心设计、完整支持 JSON Schema 各主要 draft04/06/07/2019-09/2020-12的 Go 实现。读完本文你将掌握它的编译器 APICompiler、格式与内容断言、自定义词汇Vocabulary、可内省的层级化错误模型以及随库发布的命令行工具jv的全部用法同时我们也会结合仓库源码说明 Docker CLI 是如何把它用于 Compose 文件校验的。说明本文仓库中实际 vendor 的版本为 v6.0.3见 vendor/modules.txt而jv命令行工具独立版本为 v0.7.0。一、库的整体能力一览根据 README.md 的“Library Features”该库的能力可归纳为六个方面能力类别说明规范符合度通过 JSON-Schema-Test-Suite排除 optional 用例覆盖 draft-04 / draft-06 / draft-07 / draft/2019-09 / draft/2020-12循环陷阱检测检测$schema循环与校验引用循环避免校验过程死循环自定义扩展自定义$schemaURL、自定义正则引擎、自定义格式format、自定义词汇vocabulary格式断言draft ≥ 2019-09 需显式开启内置常见格式支持自定义格式注册内容断言draft ≥ 7 需显式开启支持contentEncoding/contentMediaType/contentSchema错误模型错误可内省、具有层级结构可用#作 alt 展示输出支持 flag / basic / detailed 三种结构从源码结构看模块划分清晰compiler.go 负责编译、validator.go 负责校验、draft.go 定义各版本规范、format.go 与 content.go 提供断言实现、output.go 负责错误输出、vocab.go 提供自定义词汇接口。二、核心 APICompiler 编译 Schema 并执行校验2.1 编译与校验的基本流程该库的理念是“编译一次、校验多次”先用jsonschema.NewCompiler()创建编译器调用Compile(loc)把 Schema 编译为*Schema之后用Schema.Validate(v)反复校验任意 JSON 值。import github.com/santhosh-tekuri/jsonschema/v6 func main() { // 1. 编译 schema.json支持文件路径或 file:// URL c : jsonschema.NewCompiler() schema, err : c.Compile(schema.json) if err ! nil { panic(err) } // 2. 校验任意 JSON 值可为 map[string]any / []any / 基础类型 var doc any // 通过 jsonschema.UnmarshalJSON 解析得到 if err : schema.Validate(doc); err ! nil { panic(err) // err 为 *jsonschema.ValidationError可内省 } }入口 API 定义见 compiler.goCompiler结构体与 compiler.goCompile方法。Schema是编译后的表示内部按关键字把约束拆解为字段如Types、Properties、Items、Minimum等定义见 schema.go。若希望在全局变量初始化阶段编译可用MustCompile(loc)编译失败时直接 panic见 compiler.go。2.2 数值精度与 JSON 解析v6/loader.go 提供了UnmarshalJSON(r io.Reader) (any, error)它使用json.Decoder的UseNumber()保证大整数/高精度小数不被float64截断同时会检查顶层值之后是否还有多余内容。数值比较在内部通过big.Rat进行见 validator.go因此multipleOf、minimum等关键字对超大数值也保持精确。2.3 默认 Draft 与多版本选择Schema 依据其$schema字段选择规范版本若缺失则默认使用库当前支持的最新 draftdraft/2020-12。为保证长期行为稳定文档建议显式指定默认 draftc : jsonschema.NewCompiler() c.DefaultDraft(jsonschema.Draft4) // 缺失 $schema 时按 draft-04 编译各版本定义见 draft.goDraft4、Draft6、Draft7、Draft2019、Draft2020其中 draft ≥ 2019 引入$defs、dependentSchemas、unevaluatedProperties/Items、contentSchema、$vocabulary等关键字draft-2020 进一步引入prefixItems。每个版本的元模式metaschema已内嵌在库中见 metaschemas 目录无需联网即可加载。2.4 资源加载与引用解析AddResource(url string, doc any)把内存中的 JSON 文档注册为可被$ref引用的资源见 compiler.go。UseLoader(loader URLLoader)替换默认的资源加载器。默认实现FileLoader只支持文件 URLSchemeURLLoader可按 scheme 分发到多个加载器见 loader.go。引用解析时库会先把文档拆分为“资源”resource并收集锚点anchor支持$anchor、$dynamicAnchor、$dynamicRef、$recursiveRef等相关逻辑见 root.go。三、格式断言Format Assertions3.1 开关与默认行为format关键字在 JSON Schema 中默认只作“注解”而非“断言”。本库通过Compiler.AssertFormat()显式开启格式校验c : jsonschema.NewCompiler() c.AssertFormat() // 强制启用格式断言各版本的默认行为见 compiler.godraft-07默认启用draft/2019-09默认禁用除非元模式声明format词汇为必需draft/2020-12默认禁用除非元模式声明format-assertion词汇为必需。3.2 内置格式清单内置格式注册在 format.go与 README 中“built-in formats”一一对应类别格式标识符regex、uuid网络ipv4、ipv6、hostname、email时间date、time、date-time、duration指针json-pointer、relative-json-pointerURI 族uri、uri-reference、uri-template、iri、iri-reference其他period、semver每个格式在 format.go 中都有独立实现函数例如uuid按 RFC 4122 校验 8-4-4-4-12 的十六进制分组、ipv4拒绝前导零与越界十进制、semver按语义化版本语法校验主/次/修订号及预发布与构建元数据。若格式值类型不匹配如对数字应用date校验器直接放行。3.3 注册自定义格式type MyFormat struct{} func (MyFormat) Validate(v any) error { s, ok : v.(string) if !ok { return nil } if !strings.HasPrefix(s, my-) { return fmt.Errorf(must start with my-) } return nil } c : jsonschema.NewCompiler() c.RegisterFormat(jsonschema.Format{Name: my-format, Validate: MyFormat{}.Validate})注意名为regex的格式不可被覆盖见 compiler.go对 draft ≥ 2019-09格式断言默认关闭需要配合AssertFormat()。四、内容断言Content Assertions内容断言用于校验字符串“内部承载的内容”由三个关键字构成contentEncoding、contentMediaType、contentSchema见 compiler.go。默认全部关闭需调用Compiler.AssertContent()开启适用于 draft ≥ 7。4.1 内置实现contentEncoding: base64内置解码器使用标准 base64 解码见 content.go。contentMediaType: application/json内置媒体类型可校验字节流是否为合法 JSON并支持反序列化为 JSON 值以继续套用contentSchema见 content.go。一个典型场景是“字符串里嵌 JSON再对它做模式校验”{ type: string, contentEncoding: base64, contentMediaType: application/json, contentSchema: { type: object, required: [name] } }此时实例值必须是 base64 编码的 JSON 字符串解码后还需满足contentSchema。校验器实现位于 validator.go先按contentEncoding解码再按contentMediaType验证或反序列化最后用contentSchema校验反序列化结果。4.2 注册自定义内容断言// 自定义 contentEncoding c.RegisterContentEncoding(jsonschema.Decoder{ Name: hex, Decode: func(s string) ([]byte, error) { /* 十六进制解码 */ }, }) // 自定义 contentMediaType c.RegisterContentMediaType(jsonschema.MediaType{ Name: application/x-custom, Validate: func(b []byte) error { /* 字节校验 */ }, })接口定义见 content.goDecoder需提供DecodeMediaType需提供Validate若该媒体类型兼容 JSON 还可提供UnmarshalJSON供contentSchema使用。五、自定义正则引擎默认情况下pattern、patternProperties等关键字使用 Go 标准库regexp。若需要 ECMA-262 等更贴近 JSON Schema 规范语义的正则语法可替换引擎type MyRegexpEngine struct{} // 实现 jsonschema.RegexpEngine 接口 // func (e MyRegexpEngine) Compile(s string) (jsonschema.Regexp, error) c : jsonschema.NewCompiler() c.UseRegexpEngine(MyRegexpEngine{}.Compile) // 必须在编译任何 Schema 之前调用接口定义见 compiler.goRegexp只需实现MatchString与String。六、自定义词汇Vocabulary与混合方言6.1 基于词汇的校验体系draft/2019-09 起JSON Schema 以“词汇vocabulary”为单位组织关键字。本库将各关键字按词汇归属编译见 objcompiler.go例如 draft-2020-12 支持core、applicator、unevaluated、validation、meta-data、format-annotation、format-assertion、content等词汇见 draft.go。6.2 注册自定义词汇通过Compiler.RegisterVocabulary注册并在 Schema 中用$vocabulary声明draft ≥ 2019-09或通过AssertVocabs()强制启用draft ≤ 7vocab : jsonschema.Vocabulary{ URL: https://example.com/vocab/my-vocab, Schema: myVocabSchema, // 校验本词汇关键字语法的模式 Subschemas: []jsonschema.SchemaPath{}, // 本词汇引入的子模式位置 Compile: func(ctx *jsonschema.CompilerContext, obj map[string]any) (jsonschema.SchemaExt, error) { // 从 obj 中提取本词汇的关键字编译为 SchemaExt return nil, nil }, } c : jsonschema.NewCompiler() c.RegisterVocabulary(vocab) c.AssertVocabs() // draft ≤ 7 时需要此开关Vocabulary与SchemaExt接口定义见 vocab.go。SchemaExt.Validate在校验阶段被调用可通过ValidatorContext报告错误、标记已评估属性/元素或递归校验子值见 vocab.go。SchemaPath用于声明子模式在文档中的可能位置如properties/*、items/[]定义见 position.go。6.3 混合方言支持不同资源可以声明不同的$schema库会按资源粒度记录方言dialect并在引用解析时正确切换见 roots.go从而在同一份 Schema 文档树中混用多个 draft 关键字。七、错误模型可内省、有层级的 ValidationError7.1 错误结构校验失败时Schema.Validate返回*jsonschema.ValidationError定义见 validator.go包含四个字段SchemaURL出错位置的绝对已解引用Schema 位置InstanceLocation实例中出错值的位置token 数组可拼成 JSON PointerErrorKind错误种类Causes嵌套子错误。ErrorKind是接口KeywordPath()与LocalizedString(*message.Printer)全部内建错误种类位于 kind/kind.go例如Type、Enum、Const、Required、Minimum、UniqueItems、AnyOf、OneOf、RefCycle等。应用侧可用errors.AsType*jsonschema.ValidationError提取后逐层遍历实现“最具体错误”定位Docker CLI 的 Compose 校验正是这样做的见下文。7.2 三种结构化输出除文本化错误信息外ValidationError还提供三种 JSON 友好的结构化输出见 output.go输出方法结构flagFlagOutput()仅{valid: false}basicBasicOutput()扁平列表每个输出单元带keywordLocation、instanceLocation、errordetailedDetailedOutput()按 Schema 结构组织的嵌套树含absoluteKeywordLocation输出单元OutputUnit字段见 output.go。README 提到错误层级可用#作“alternative display”——即LocalizedGoString会在错误信息中附上简写的 Schema 位置如S#/properties/name帮助定位。八、命令行工具 jv独立版本 v0.7.08.1 安装go install github.com/santhosh-tekuri/jsonschema/cmd/jvlatestjv与库本体独立版本号管理git tag 形如cmd/jv/v0.7.0。8.2 用法与选项Usage: jv [OPTIONS] SCHEMA [INSTANCE...]选项说明-c, --assert-content对 draft ≥ 7 启用内容断言-f, --assert-format对 draft ≥ 2019 启用格式断言--cacert pem-file使用指定 PEM 文件验证对端证书文件可含多个 CA 证书-d, --draft version缺失$schema时使用的 draft合法值4、6、7、2019、2020默认2020-h, --help打印帮助-k, --insecure使用不安全的 TLS 连接跳过证书验证-o, --output format输出格式simple、alt、flag、basic、detailed默认simple-q, --quiet不打印错误-v, --version打印构建信息8.3 关键行为退出码校验错误返回1用法错误返回2。同时校验 Schema 与多个实例jv schema.json a.json b.json可一次校验多个实例文件。JSON 与 YAML 均支持Schema 与实例都可以是 YAML 文件。标准输入使用-作为实例参数从 stdin 读取。quiet 模式-q不打印错误可与脚本结合仅依赖退出码-o flag输出结构化布尔结果。HTTP(S) URLSchema 位置可直接传https://...需要自定义 CA 时用--cacert跳过证书校验时用--insecure。九、仓库实战Docker CLI 如何用它校验 Compose 文件作为“锦上添花”的源码证据本仓库在 cli/compose/schema/schema.go 中直接使用了该库来校验 Compose 配置通过//go:embed data/config_schema_v*.json内嵌各版本 Compose 配置模式见 schema.goValidate(config map[string]any, version string)中读取对应版本模式 →jsonschema.UnmarshalJSON解析 →jsonschema.NewCompiler()编译 → 注册两个自定义格式ports与duration前者委托nat.ParsePortSpec后者委托time.ParseDuration见 schema.go→compiler.AddResource(schema.json, schemaDoc)→Compile→schema.Validate见 schema.go校验失败后利用errors.AsType[*jsonschema.ValidationError]提取错误通过flattenErrors展开层级并用specificity基于InstanceLocation长度挑选“最具体错误”把got X, want Y之类信息转成field: must be ...的人类可读文案见 schema.go。该校验在 Compose 加载流程中被调用见 cli/compose/loader/loader.go 的schema.Validate(configDict, configDetails.Version)是理解“先编译、后校验”与错误内省能力在真实工程中价值的最佳范例。十、循环检测与健壮性README 明确列出两项循环陷阱检测能力$schema循环加载$schema指向的元模式时维护 cycle 集合若元模式又引用回自身则报MetaSchemaCycleError见 loader.go校验循环$ref/$recursiveRef/$dynamicRef形成的引用环由scope的checkCycle在运行时拦截报RefCycle错误而非无限递归见 validator.go。同时库还提供一系列明确的错误类型LoadURLError、UnsupportedDraftError、DuplicateIDError、DuplicateAnchorError、AnchorNotFoundError、UnsupportedVocabularyError等分散于 loader.go、roots.go、root.go方便调用方精确区分失败原因。结语santhosh-tekuri/jsonschema v6 以“编译-校验”两阶段模型为核心在保持对 JSON Schema 主流版本完整支持的同时通过格式/内容断言、自定义正则、自定义词汇与结构化错误输出把扩展点都设计为公开的 Go 接口jv命令行工具则把同样的能力平移到脚本与 CI 场景。无论是直接集成到 Go 服务还是像 Docker CLI 这样用它校验配置文件的合法性你都可以基于本文的 API 与源码路径快速上手并深入调试。赞分享CLI开发工具【免费下载链接】cliThe Docker CLI项目地址https://gitcode.com/gh_mirrors/cli5/cli点击查看免费下载相关推荐Grafana Tempo 中的 JSON Schema 校验利器jsonschema v6 库特性全解与 jv CLI 实战指南Grafana Tempo 中的 JSON Schema 校验利器jsonschema v6 库特性全解与 jv CLI 实战指南 本文以 Grafana T后端可观测性链路追踪Go 语言 JSON Schema 校验全攻略gojsonschemav1.2.0库解析与实战Go 语言 JSON Schema 校验全攻略gojsonschemav1.2.0库解析与实战 本文以 KubeSphere 仓库中 vendored 的后端云原生容器编排微服务go-swagger validate 命令完全指南使用 JSON Schema 与语义规则校验 Swagger 2.0 规范go swagger validate 命令完全指南使用 JSON Schema 与语义规则校验 Swagger 2.0 规范 swagger validat代码生成开发工具后端API设计创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

python3 中的字符串(单引号、双引号、三引号)以及字符串与数字的运算

python3 中的字符串(单引号、双引号、三引号)以及字符串与数字的运算

前言 写 Python 3 的第一课往往是「字符串怎么表示」。单引号、双引号、三引号这三种写法都能创建字符串,但它们之间有没有区别、该用哪种,初学者常常是凭感觉选的。先说结论:在 Python 3 里,单引号和双引号创建的字符串没有任何语…

2026/10/12 4:11:30 阅读更多 →
别被“降AI率”带偏:让AI文本更有“人味”的5种方法

别被“降AI率”带偏:让AI文本更有“人味”的5种方法

先声明一句:这类“降AI率”的标题我刷到过很多次,自己也手痒实测过几轮。如果你是为了让AI写出来的东西更像“人话”,而不是为了在论文、作业、考核里耍小聪明,那这篇文章可以帮你省下大量瞎折腾的时间。我最后留下的是五个从根本…

2026/10/12 4:11:30 阅读更多 →
Coding Interview University:一份从零到Offer的软件工程师面试自学路线图

Coding Interview University:一份从零到Offer的软件工程师面试自学路线图

最近有几个朋友不约而同地问我同一个问题:准备软件工程师面试,到底该从哪里下手?市面上的面经、刷题网站、课程多到让人眼花缭乱,有人一头扎进 LeetCode 从第一题刷到第三百题,有人捧着系统设计书死磕,还有…

2026/10/12 4:10:30 阅读更多 →

最新新闻

PostgreSQL性能压测实战:用TPC-H标准流程构建可复现基准测试环境

PostgreSQL性能压测实战:用TPC-H标准流程构建可复现基准测试环境

1. 项目概述:为什么TPC-H是检验PostgreSQL真实能力的“压力测试仪”你刚装好PostgreSQL,跑通了第一个CREATE TABLE,连上pgAdmin点了几次查询,心里有点小得意——数据库这玩意儿,好像也没那么难?别急&#x…

2026/10/12 5:09:00 阅读更多 →
基于Spring Boot的车牌识别停车场管理系统设计与实现

基于Spring Boot的车牌识别停车场管理系统设计与实现

1. 项目概述与选题价值1.1 这个系统到底解决什么问题我第一次看到这个题目的时候,第一反应是:这又是一个“典型的毕业设计式管理系统”?因为现在网上关于停车场、图书馆、宿舍管理这类CRUD项目太多了,很多同学开题时随手挑一个&am…

2026/10/12 5:09:00 阅读更多 →
Spring Boot农事管理系统毕业设计:从数据库建模到核心功能实现

Spring Boot农事管理系统毕业设计:从数据库建模到核心功能实现

写这个题目前,我先说句实在话:Spring Boot 农事管理系统,这个搭配在国内农业信息化方向的毕业设计里,已经算得上“经典款”了。经典意味着什么?意味着参考资料好找、技术路线成熟、踩坑记录也很多,不至于让…

2026/10/12 5:09:00 阅读更多 →
MATLAB快速谱相干:从一维时间序列到旋转机械多通道分析

MATLAB快速谱相干:从一维时间序列到旋转机械多通道分析

前几天我在一个设备诊断交流群里看到有人贴图:同一条轴上的两路振动信号,普通幅值谱看着都差不多,在某个轴承故障特征频率附近却同时出现了一处明显的相干峰。下面跟了几条回复,有人问“相干峰到底代表什么”,有人说“…

2026/10/12 5:09:00 阅读更多 →
SpringBoot+Vue+MySQL旅游网站毕设项目全解析:从数据库设计到部署答辩

SpringBoot+Vue+MySQL旅游网站毕设项目全解析:从数据库设计到部署答辩

每年毕业季我都会收到大量和“旅游网站”相关的咨询,这套 SpringBootVueMySQL 的某北方城市特色旅游网站平台,属于完成度很高的一类毕设项目。它带了完整数据库脚本、论文文档和部署说明,代码结构比多数网上流传的“半成品”要规矩得多。这篇…

2026/10/12 5:09:00 阅读更多 →
微客AI助手答疑:AI客服的会话记录存在哪?留存位置与合规要点

微客AI助手答疑:AI客服的会话记录存在哪?留存位置与合规要点

给商家配微客AI助手的时候,被问过的最认真的一组问题来自一位做母婴用品的店主。她问的不是价格也不是功能,而是:客户的聊天记录存在哪?谁能看到?会不会被拿去做别的?说实话,这三个问题比大多数…

2026/10/12 5:08:00 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 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/11 10:45:37 阅读更多 →
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/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练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/11 14:36:54 阅读更多 →