news 2026/11/2 10:42:47

C++字体库开发之fontconfig使用五

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++字体库开发之fontconfig使用五
代码
#include <cassert> #include <algorithm> #include "fontconfig/fontconfig.h" #include <stdexcept> #include <iostream> #define HAS_FALLBACK_CONFIGURATION enum FontStyle : uint16_t { Regular = 0, Italic = 0x01, Bold = 0x02, }; enum class FontConfigSearchFlags : uint16_t { None = 0, MatchPostScriptName = 0x01, ///< Match postscript font name. The default is match family name. This search may be more specific }; struct FontConfigSearchParams { uint16_t Style; uint16_t Flags = 0; }; FcConfig* m_FcConfig; void createDefaultConfig() { #ifdef _WIN32 const char* fontconf = R"(<?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <fontconfig> <dir>WINDOWSFONTDIR</dir> <dir>WINDOWSUSERFONTDIR</dir> <dir prefix="xdg">fonts</dir> <cachedir>LOCAL_APPDATA_FONTCONFIG_CACHE</cachedir> <cachedir prefix="xdg">fontconfig</cachedir> </fontconfig> )"; #elif __ANDROID__ // On android fonts are located in /system/fonts const char* fontconf = R"(<?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <fontconfig> <dir>/system/fonts</dir> <dir prefix="xdg">fonts</dir> <cachedir prefix="xdg">fontconfig</cachedir> </fontconfig> )"; #elif __LINUX__ const char* fontconf = R"(<?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <fontconfig> <dir>/usr/share/fonts</dir> <dir>/usr/local/share/fonts</dir> <dir>/usr/X11R6/lib/X11/fonts</dir> <dir>/usr/X11/lib/X11/fonts</dir> <dir>/usr/lib/X11/fonts</dir> <dir prefix="xdg">fonts</dir> <cachedir prefix="xdg">fontconfig</cachedir> </fontconfig> )"; #elif __APPLE__ // Fonts location https://stackoverflow.com/a/2557291/213871 const char* fontconf = R"(<?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <fontconfig> <dir>/System/Library/Fonts</dir> <dir>/Library/Fonts</dir> <dir>~/Library/Fonts</dir> <dir>/System/Library/Assets/com_apple_MobileAsset_Font3</dir> <dir>/System/Library/Assets/com_apple_MobileAsset_Font4"</dir> <dir prefix="xdg">fonts</dir> <cachedir prefix="xdg">fontconfig</cachedir> </fontconfig> )"; #endif #ifdef HAS_FALLBACK_CONFIGURATION // Implement the fallback as discussed in fontconfig mailing list // https://lists.freedesktop.org/archives/fontconfig/2022-February/006883.html auto config = FcConfigCreate(); if (config == nullptr) throw std::runtime_error("Could not allocate font config"); // Manually try to load the config to determine // if a system configuration exists. Tell FontConfig // to not complain if it doesn't (void)FcConfigParseAndLoad(config, nullptr, FcFalse); auto configFiles = FcConfigGetConfigFiles(config); if (FcStrListNext(configFiles) == nullptr) { // No system config found, supply a fallback configuration if (!FcConfigParseAndLoadFromMemory(config, (const FcChar8*)fontconf, true)) { FcConfigDestroy(config); throw std::runtime_error("Could not parse font config"); } // Load fonts for the config if (!FcConfigBuildFonts(config)) { FcConfigDestroy(config); throw std::runtime_error("Could not load fonts in fontconfig"); } m_FcConfig = config; } else { // Destroy the temporary config FcStrListDone(configFiles); FcConfigDestroy(config); #endif // Default initialize a local FontConfig configuration // http://mces.blogspot.com/2015/05/how-to-use-custom-application-fonts.html m_FcConfig = FcInitLoadConfigAndFonts(); assert(m_FcConfig != nullptr); #ifdef HAS_FALLBACK_CONFIGURATION } #endif } bool getFontInfo(FcPattern* font, std::string& fontFamily, std::string& fontPath, uint16_t& style) { FcChar8* family = nullptr; FcChar8* path = nullptr; int slant; int weight; style = FontStyle::Regular; if (FcPatternGetString(font, FC_FAMILY, 0, &family) == FcResultMatch) { fontFamily = reinterpret_cast<char*>(family); if (FcPatternGetString(font, FC_FILE, 0, &path) == FcResultMatch) { fontPath = reinterpret_cast<char*>(path); if (FcPatternGetInteger(font, FC_SLANT, 0, &slant) == FcResultMatch) { if (slant == FC_SLANT_ITALIC || slant == FC_SLANT_OBLIQUE) style |= FontStyle::Italic; if (FcPatternGetInteger(font, FC_WEIGHT, 0, &weight) == FcResultMatch) { if (weight >= FC_WEIGHT_BOLD) style |= FontStyle::Bold; return true; } } // free( file ); } // free( family ); } return false; } std::string SearchFontPath(const std::string_view fontPattern, const FontConfigSearchParams& params, unsigned& faceIndex) { FcPattern* pattern; FcPattern* matched; FcResult result = FcResultMatch; FcValue value; pattern = FcPatternCreate(); if (pattern == nullptr) return ("FcPatternCreate returned NULL"); // Build a pattern to search using postscript name, bold and italic if (params.Flags != (uint16_t)FontConfigSearchFlags::MatchPostScriptName) FcPatternAddString(pattern, FC_FAMILY, (const FcChar8*)fontPattern.data()); else FcPatternAddString(pattern, FC_POSTSCRIPT_NAME, (const FcChar8*)fontPattern.data()); if (params.Style) { bool isItalic = (params.Style & FontStyle::Italic) == FontStyle::Italic; bool isBold = (params.Style & FontStyle::Bold) == FontStyle::Bold; FcPatternAddInteger(pattern, FC_WEIGHT, (isBold ? FC_WEIGHT_BOLD : FC_WEIGHT_MEDIUM)); FcPatternAddInteger(pattern, FC_SLANT, (isItalic ? FC_SLANT_ITALIC : FC_SLANT_ROMAN)); } // Follow fc-match procedure which proved to be more reliable // https://github.com/freedesktop/fontconfig/blob/e291fda7d42e5d64379555097a066d9c2c4efce3/fc-match/fc-match.c#L188 if (!FcConfigSubstitute(m_FcConfig, pattern, FcMatchPattern)) { FcPatternDestroy(pattern); faceIndex = 0; return {}; } FcDefaultSubstitute(pattern); std::string path; matched = FcFontMatch(m_FcConfig, pattern, &result); if (result != FcResultNoMatch) { (void)FcPatternGet(matched, FC_FILE, 0, &value); path = reinterpret_cast<const char*>(value.u.s); (void)FcPatternGet(matched, FC_INDEX, 0, &value); faceIndex = (unsigned)value.u.i; } FcPatternDestroy(pattern); FcPatternDestroy(matched); #if _WIN32 // Font config in Windows returns unix conventional path // separator. Fix it std::replace(path.begin(), path.end(), '/', '\\'); #endif return path; } void testSingleFont(FcPattern* font) { std::string fontFamily; std::string fontPath; uint16_t style; FontConfigSearchParams fcParams; if (getFontInfo(font, fontFamily, fontPath, style)) { unsigned faceIndex; fcParams.Style = style; fontPath = SearchFontPath(fontFamily, fcParams, faceIndex); if (fontPath.length() != 0) { std::cout << "fontPath:" << fontPath << ",family:" << fontFamily << ",style:" << style << std::endl; } } // 简单打印 FcPatternPrint(font); //自定义格式打印 const FcChar8* format = (const FcChar8*)"%{=fclist}\n"; //const FcChar8* format = (const FcChar8*)"%{=unparse}\n"; //const FcChar8* format = (const FcChar8*)"%{=fcmatch}\n"; auto s = FcPatternFormat(font, format); std::cout << s << "\n"; FcStrFree(s); } // 测试字体安装目录 void listInstallFonts() { // Get all installed fonts auto pattern = FcPatternCreate(); auto objectSet = FcObjectSetBuild(FC_FAMILY, FC_STYLE, FC_FILE, FC_SLANT, FC_WEIGHT, nullptr); auto fontSet = FcFontList(nullptr, pattern, objectSet); FcObjectSetDestroy(objectSet); FcPatternDestroy(pattern); if (fontSet == nullptr) { printf("Unable to search for fonts"); return; } std::cout << "font count " << fontSet->nfont << std::endl; for (int i = 0; i < fontSet->nfont; i++) testSingleFont(fontSet->fonts[i]); FcFontSetDestroy(fontSet); } static FcStrSet* processed_dirs; static int scanDirs(FcStrList* list, FcConfig* config, FcBool force, FcBool really_force, FcBool verbose, FcBool error_on_no_fonts, int* changed) { int ret = 0; const FcChar8* dir; FcStrSet* subdirs; FcStrList* sublist; FcCache* cache; struct stat statb; FcBool was_valid, was_processed = FcFalse; int i; const FcChar8* sysroot = FcConfigGetSysRoot(config); /* * Now scan all of the directories into separate databases * and write out the results */ while ((dir = FcStrListNext(list))) { if (verbose) { if (sysroot) printf("[%s]", sysroot); printf("%s: ", dir); fflush(stdout); } if (FcStrSetMember(processed_dirs, dir)) { if (verbose) printf(("skipping, looped directory detected\n")); continue; } FcChar8* rooted_dir = NULL; if (sysroot) { rooted_dir = FcStrPlus(sysroot, dir); } else { rooted_dir = FcStrCopy(dir); } if (stat((char*)rooted_dir, &statb) == -1) { switch (errno) { case ENOENT: case ENOTDIR: if (verbose) printf(("skipping, no such directory\n")); break; default: fprintf(stderr, "\"%s\": ", dir); perror(""); ret++; break; } FcStrFree(rooted_dir); rooted_dir = NULL; continue; } FcStrFree(rooted_dir); rooted_dir = NULL; if ((statb.st_mode & 0xF000) != 0x4000) { fprintf(stderr, ("\"%s\": not a directory, skipping\n"), dir); continue; } was_processed = FcTrue; if (really_force) { FcDirCacheUnlink(dir, config); } cache = NULL; was_valid = FcFalse; if (!force) { //if (FcFileIsDir(arg)) // cache = FcDirCacheLoad(arg, config, &rooted_dir); //else // cache = FcDirCacheLoadFile(arg, &statb); cache = FcDirCacheLoad(dir, config, NULL); if (cache) was_valid = FcTrue; } if (!cache) { (*changed)++; cache = FcDirCacheRead(dir, FcTrue, config); if (!cache) { fprintf(stderr, ("\"%s\": scanning error\n"), dir); ret++; continue; } } if (was_valid) { if (verbose) printf(("skipping, existing cache is valid: %d fonts, %d dirs\n"), FcCacheNumFont(cache), FcCacheNumSubdir(cache)); } else { if (verbose) printf(("caching, new cache contents: %d fonts, %d dirs\n"), FcCacheNumFont(cache), FcCacheNumSubdir(cache)); if (!FcDirCacheValid(dir)) { fprintf(stderr, ("%s: failed to write cache\n"), dir); (void)FcDirCacheUnlink(dir, config); ret++; } } subdirs = FcStrSetCreate(); if (!subdirs) { fprintf(stderr, ("%s: Can't create subdir set\n"), dir); ret++; FcDirCacheUnload(cache); continue; } for (i = 0; i < FcCacheNumSubdir(cache); i++) FcStrSetAdd(subdirs, FcCacheSubdir(cache, i)); FcDirCacheUnload(cache); sublist = FcStrListCreate(subdirs); FcStrSetDestroy(subdirs); if (!sublist) { fprintf(stderr, ("%s: Can't create subdir list\n"), dir); ret++; continue; } FcStrSetAdd(processed_dirs, dir); ret += scanDirs(sublist, config, force, really_force, verbose, error_on_no_fonts, changed); FcStrListDone(sublist); } if (error_on_no_fonts && !was_processed) ret++; return ret; } static FcBool cleanCacheDirectories(FcConfig* config, FcBool verbose) { FcStrList* cache_dirs = FcConfigGetCacheDirs(config); FcChar8* cache_dir; FcBool ret = FcTrue; if (!cache_dirs) return FcFalse; while ((cache_dir = FcStrListNext(cache_dirs))) { if (!FcDirCacheClean(cache_dir, verbose)) { ret = FcFalse; break; } } FcStrListDone(cache_dirs); return ret; } // 测试字体缓存目录 int testFontCache(const char* fontdirs) { //if (!FcFileIsDir((const FCChar8*)fontdirs)) // return 0; FcStrSet* dirs; FcStrList* list; FcBool verbose = FcFalse; FcBool force = FcFalse; FcBool really_force = FcFalse; FcBool systemOnly = FcFalse; FcBool error_on_no_fonts = FcFalse; FcConfig* config; FcChar8* sysroot = NULL; int changed; int ret; if (systemOnly) FcConfigEnableHome(FcFalse); if (sysroot) { FcConfigSetSysRoot(NULL, sysroot); FcStrFree(sysroot); config = FcConfigGetCurrent(); } else { config = FcInitLoadConfig(); } if (!config) { fprintf(stderr, ("%s: Can't initialize font config library\n"), fontdirs); return 1; } FcConfigSetCurrent(config); if (fontdirs) { dirs = FcStrSetCreate(); if (!dirs) { fprintf(stderr, ("%s: Can't create list of directories\n"), fontdirs); return 1; } if (!FcStrSetAddFilename(dirs, (FcChar8*)fontdirs)) { fprintf(stderr, ("%s: Can't add directory\n"), fontdirs); return 1; } list = FcStrListCreate(dirs); FcStrSetDestroy(dirs); } else list = FcConfigGetFontDirs(config); if ((processed_dirs = FcStrSetCreate()) == NULL) { fprintf(stderr, ("Out of Memory\n")); return 1; } if (verbose) { const FcChar8* dir; printf("Font directories:\n"); while ((dir = FcStrListNext(list))) { printf("\t%s\n", dir); } FcStrListFirst(list); } changed = 0; ret = scanDirs(list, config, force, really_force, verbose, error_on_no_fonts, &changed); FcStrListDone(list); /* * Try to create CACHEDIR.TAG anyway. * This expects the fontconfig cache directory already exists. * If it doesn't, it won't be simply created. */ FcCacheCreateTagFile(config); FcStrSetDestroy(processed_dirs); cleanCacheDirectories(config, verbose); FcConfigDestroy(config); FcFini(); /* * Now we need to sleep a second (or two, to be extra sure), to make * sure that timestamps for changes after this run of fc-cache are later * then any timestamps we wrote. We don't use gettimeofday() because * sleep(3) can't be interrupted by a signal here -- this isn't in the * library, and there aren't any signals flying around here. */ /* the resolution of mtime on FAT is 2 seconds */ //if (changed) // sleep(2); if (verbose) printf("%s: %s\n", fontdirs, ret ? "failed" : "succeeded"); return ret; } // 配置列表 void testConfigList() { FcConfig* config; FcConfigFileInfoIter iter; config = FcConfigGetCurrent(); FcConfigFileInfoIterInit(config, &iter); do { FcChar8* name, * desc; FcBool enabled; if (FcConfigFileInfoIterGet(config, &iter, &name, &desc, &enabled)) { printf("%c %s: %s\n", enabled ? '+' : '-', name, desc); FcStrFree(name); FcStrFree(desc); } } while (FcConfigFileInfoIterNext(config, &iter)); FcFini(); } // 字体查找 void queryFont(const FcChar8 *fontdir) { auto fs = FcFontSetCreate(); // FcFreeTypeQuery if (!FcFreeTypeQueryAll(fontdir, 0, NULL, NULL, fs)) { fprintf(stderr, "Can't query face %u of font file %s\n", -1, fontdir); return; } for (int i = 0; i < fs->nfont; i++) { FcPattern* pat = fs->fonts[i]; FcPatternPrint(pat); //FcPatternDel(pat, FC_CHARSET); //FcPatternDel(pat, FC_LANG); } FcFontSetDestroy(fs); FcFini(); } // 字体目录解析 void scanFont(const FcChar8* file) { auto fs = FcFontSetCreate(); if (!FcFileIsDir(file)) FcFileScan(fs, NULL, NULL, NULL, file, FcTrue); else { FcStrSet* dirs = FcStrSetCreate(); FcStrList* strlist = FcStrListCreate(dirs); do { FcDirScan(fs, dirs, NULL, NULL, file, FcTrue); } while ((file = FcStrListNext(strlist))); FcStrListDone(strlist); FcStrSetDestroy(dirs); } for (int i = 0; i < fs->nfont; i++) { FcPattern* pat = fs->fonts[i]; FcPatternPrint(pat); //FcPatternDel(pat, FC_CHARSET); //FcPatternDel(pat, FC_LANG); } FcFontSetDestroy(fs); FcFini(); } void test() { //listInstallFonts(); //testFontCache("C:\\Fonts"); //testConfigList(); //queryFont((const FcChar8*)"C:\\Fonts\\LiberationSans.ttc"); //scanFont((const FcChar8*)"C:\\Fonts"); // 待续...... // FcConfigSetCurrent FcConfigUptoDate FcConfigGetCache FcConfigGetRescanInterval FcConfigGetFonts FcConfigAppFontAddFile FcConfigAppFontAddDir // FcConfigAppFontClear FcDirSave FcDirCacheRescan FcFontSetList FcAtomicLock FcFontSetMatch FcFontRenderPrepare FcFontSetSort // FcFontSetSortDestroy FcPatternHash }
输出


fontPath:C:\Windows\fonts\GOTHIC.TTF,family:Century Gothic,style:0
Pattern has 5 elts (size 16)
family: "Century Gothic"(s)
style: "Regular"(s) "Normal"(s) "obyčejné"(s) "Standard"(s) "Κανονικά"(s) "Normaali"(s) "Normál"(s) "Normale"(s) "Standaard"(s) "Normalny"(s) "Обычный"(s) "Normálne"(s) "Navadno"(s) "Arrunta"(s)
slant: 0(i)(s)
weight: 80(f)(s)
file: "C:/Windows/fonts\GOTHIC.TTF"(s)

C:/Windows/fonts\GOTHIC.TTF: Century Gothic:style=Regular,Normal,obyčejné,Standard,Κανονικά,Normaali,Normál,Normale,Standaard,Normalny,Обычный,Normálne,Navadno,Arrunta:slant=0:weight=80

Pattern has 26 elts (size 32)
family: "Liberation Sans"(s)
familylang: "en"(s)
style: "Bold"(s)
stylelang: "en"(s)
fullname: "Liberation Sans Bold"(s)
fullnamelang: "en"(s)
slant: 0(i)(s)
weight: 200(f)(s)
width: 100(f)(s)
foundry: "1ASC"(s)
file: "C:\Fonts\LiberationSans-Bold.ttf"(s)
index: 0(i)(s)
outline: True(s)
scalable: True(s)
charset:
0000: 00000000 ffffffff ffffffff 7fffffff 00000000 ffffffff ffffffff ffffffff
0001: ffffffff ffffffff ffffffff ffffffff 00040000 00000000 00000000 fc000000
0002: 0f000000 00000000 00000000 00000000 00000000 00000000 3f0002c0 00000000
0003: 00000000 00000000 00000000 40000000 ffffd7f0 fffffffb 00007fff 00000000
0004: ffffffff ffffffff ffffffff 000c0000 00030000 00000000 00000000 00000000
001e: 00000000 00000000 00000000 00000000 0000003f 00000000 00000000 000c0000
0020: 7fbb0000 560d0047 00000010 80000000 00000000 00001098 00000000 00000000
0021: 00480020 00004044 78000000 00000000 003f0000 00000100 00000000 00000000
0022: c6268044 00000a00 00000100 00000033 00000000 00000000 00000000 00000000
0023: 00010004 00000003 00000000 00000000 00000000 00000000 00000000 00000000
0025: 11111005 10101010 ffff0000 00001fff 000f1111 14041c03 03008c10 00000040
0026: 00000000 1c000000 00000005 00001c69 00000000 00000000 00000000 00000000
00f0: 00000026 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00fb: 00000006 00000000 00000000 00000000 00000000 00000000 00000000 00000000
(s)
lang: aa|af|av|ay|be|bg|bi|br|bs|ca|ce|ch|co|cs|cy|da|de|el|en|eo|es|et|eu|fi|fj|fo|fr|fur|fy|gd|gl|gv|ho|hr|hu|ia|id|ie|ik|io|is|it|ki|kl|kum|la|lb|lez|lt|lv|mg|mh|mk|mo|mt|nb|nds|nl|nn|no|nr|nso|ny|oc|om|os|pl|pt|rm|ro|ru|se|sel|sk|sl|sma|smj|smn|so|sq|sr|ss|st|sv|sw|tk|tl|tn|tr|ts|uk|uz|vo|vot|wa|wen|wo|xh|yap|zu|an|crh|csb|fil|hsb|ht|jv|kj|ku-tr|kwm|lg|li|ms|na|ng|pap-an|pap-aw|rn|rw|sc|sg|sn|su|za(s)
fontversion: 70123(i)(s)
capability: "otlayout:DFLT otlayout:cyrl otlayout:grek otlayout:latn"(s)
fontformat: "TrueType"(s)
decorative: False(s)
postscriptname: "LiberationSans-Bold"(s)
color: False(s)
symbol: False(s)
variable: False(s)
fonthashint: True(s)
order: 0(i)(s)

参考

C++字体库开发之字符显示四-CSDN博客

https://github.com/freedesktop/fontconfig

https://github.com/ShiftMediaProject/fontconfig

https://github.com/benoitkugler/textlayout

https://github.com/benoitkugler/textprocessing

https://github.com/go-text/typesetting


创作不易,小小的支持一下吧!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 10:42:06

基于STC89C51的多功能红外避障智能小车设计

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

作者头像 李华
网站建设 2026/11/2 10:38:27

PLFM雷达FPGA实现:高速ADC采样与多端口DDR设计

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

作者头像 李华
网站建设 2026/11/2 10:38:09

PLFM调制原理与FMCW雷达信号处理实战

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

作者头像 李华
网站建设 2026/11/2 10:37:53

SPI物理隔离设计:解决工业现场共地干扰实战指南

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

作者头像 李华
网站建设 2026/11/2 10:37:31

平台级雷达监控系统PLFM_RADAR:从指标采集到异常检测的落地实践

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

作者头像 李华
网站建设 2026/11/2 10:36:16

PLFM_RADAR:FPGA实现相控阵雷达数字处理系统

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

作者头像 李华