ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

解码prism4cj核心标记化算法:greedy贪婪匹配与lookbehind如何实现精准代码标记

解码prism4cj核心标记化算法:greedy贪婪匹配与lookbehind如何实现精准代码标记 解码prism4cj核心标记化算法greedy贪婪匹配与lookbehind如何实现精准代码标记【免费下载链接】prism4cj一个轻量的语法高亮库项目地址: https://gitcode.com/Cangjie-TPC/prism4cjprism4cj 是一个用仓颉语言编写的轻量级语法高亮库它的核心是一套代码标记化tokenize算法把原始代码文本切分成不同类型的 Token关键词、字符串、注释、运算符……为语法高亮渲染做准备。这篇文章带你解码其中的两大关键机制——greedy 贪婪匹配与lookbehind 后顾断言看看它们如何配合实现精准的代码标记。为什么语法高亮需要标记化编辑器里的代码着色本质上是两步标记Tokenize按语法规则把文本切成带类型的片段渲染Render按类型给片段上色。以一张 Java 注解关系图为例图中Target、Retention这些注解、Controller这些类名在源码中都需要被识别成不同 Token 才能正确高亮prism4cj 把怎么切抽象为三层结构全部定义在 src/prism/ 下概念含义源码位置Grammar语法一种语言的全部规则集合grammar.cjToken标记一类语法元素如keyword、stringtoken.cjPattern模式一条正则 两个关键开关lookbehind和greedypattern.cjpublic abstract class Pattern : Hashable EquatablePattern ToString { public func regex(): Regex public func lookbehind(): Bool public func greedy(): Bool }整个引擎的流转如下完整架构图见官方仓库文档入口只有两个方法grammar(name)按名字取语法tokenize(text, grammar)执行标记返回ArrayListNodeprism.cj。所有玄机都藏在matchGrammar私有方法里。greedy 贪婪匹配让字符串和注释跨过已标记片段先想一个问题C 语言的字符串abc // not a comment里含//注释规则不该误伤它反过来注释里的字符串也不该被标记。普通做法是在当前文本片段内做正则匹配这就是 prism4cj 的默认分支见 prism.cj但片段是动态切分的边界不好控制。prism4cj 的答案是greedy 模式把匹配范围扩大到整段原文但保证不越过已有标记的边界。核心逻辑在 prism.cjif (greedy i ! entries.size - 1) { matcher regex.matcher(text) // 直接在原文上匹配 matcher.setRegion(position, textLength) // 从当前扫描位置开始 matchData matcher.find() ... // 推进 i直到覆盖整个匹配区间 // 遇到非贪婪语法节点即停绝不跨过它 while (k len (p to || (!isSyntaxNode(entries.get(k)...) !isGreedyNode(entries.get(k - 1) ...)))) { ... } }三个要点原文级匹配regex.matcher(text)setRegion把搜索窗口固定在当前位置 → 文末天然支持跨节点命中边界保护推进索引时一旦撞上普通非 greedy语法节点就停下保证先到先得——注释规则不会吃掉字符串规则已标记的内容贪心标记命中的片段包成Syntax节点并带上greedytrue标记node.cj供其他 greedy 模式识别边界one-shot 续扫greedy 命中后以oneShottrue递归调用自身prism.cj在同一 Token 上继续扫描后续片段扫完即停。 一句话总结greedy 我可以跨片段找匹配但不许踩别人的地盘。lookbehind 后顾保留前缀上下文却不把前缀算进匹配greedy解决在哪匹配lookbehind解决匹配到哪。典型的词边界需求标记#include后跟的字符串时正则必须看到#include前缀才肯出手但最终 Token 里只该有字符串本身。若直接捕获前缀前缀会被切进 Token 里高亮颜色就串了。prism4cj 的解法约定捕获组 1 固定是前缀命中后把第 1 组的长度从起点扣除prism.cjlet lookbehindLength: Int64 matchRealData2.matchString(1).size let begin02 matchRealData2.matchPosition().start greedyAdd lookbehindLength mat matchRealData2.matchString()[lookbehindLength..] // 前缀被剥掉剥掉前缀后代码把命中点前后分别拆成before纯文本和after剩余文本插回节点列表prism.cj保证原文一个字都不丢。真实案例C 语言宏的标记规则看 prism_c.cjC 语法在string之前插入了macroToken一个 Pattern 同时用上了 lookbehind、别名和嵌套语法Prism.pattern( Regex((^\\s*)#\\s*[a-z](?:[^\\r\\n\\\\]|\\\\(?:\\r\\n|[\\s\\S]))*, RegexFlag.MultiLine), true, // lookbehind(^\\s*) 前缀不参与标记 false, // 非 greedy片段内匹配 property, // 别名按 property 样式着色 insideGrammar )其内部嵌套的string规则同样带lookbehindtrue(#\\s*include\\s*)作为捕获组 1于是#include stdio.h中#include保持普通文本色、stdio.h单独按字符串上色——这正是精准标记的来源。同类技巧在directive规则中复现捕获(#\\s*)前缀只标记define、include等指令词并别名为keywordprism_c.cj。inside 嵌套标记Token 里的 TokenPattern还支持第四个能力inside命中片段若携带子语法就递归调用tokenize(mat, inside)再做一轮标记prism.cj。上面的 C 宏就是例子——宏整体是一个 Token宏内部的字符串、指令又按子语法规则细分。配合 grammar_utils.cj 的extendGrammar克隆父语言语法并覆写规则和insertBeforeToken按路径插入规则规则顺序即优先级顺序各语言包能像积木一样基于clike等基座语法快速搭建例如 prism_cpp.cj、prism_csharp.cj。三步上手调用标记化接口完整 API 说明见 doc/feature_api.md典型用法只有三步可参考 test_c_readme.cjvar prism Prism(GrammarLocatorGrammarUtils()) match (prism.grammar(c)) { case Some(v) TestUtils.assertCase(c, prism.tokenize(c.input, v)) case None () }Prism(GrammarLocatorGrammarUtils())构造引擎grammar(c)取语言规则tokenize(text, grammar)得到Node列表交给 visitor.cj 的访问者遍历渲染即可。语言包覆盖了 C、C、Java、JavaScript、Python、Go、Rust、SQL、YAML、Markdown 等 25 种src/languages/每种语言都配套了 HLT 特征用例与 LLT 自测用例test/HLT/function/languages/。常见问题速答Qgreedy 会不会越界覆盖别的标记不会。推进索引时遇到非 greedy 的Syntax节点立即停止isGreedyNode判定prism.cj先到先得。Qlookbehind 的正则怎么写捕获组 1 写前缀组 2 之后写真正要标记的内容引擎自动扣除第 1 组长度前缀保留在纯文本中不被着色。Q规则顺序重要吗非常重要。matchGrammar按Grammar.tokens()的顺序依次扫描prism.cj先命中者优先所以用insertBeforeToken调整顺序是常用手段。小结tokenize 主循环把文本维护成Text / Syntax节点列表按 Token 顺序逐模式扫描greedy原文级匹配 边界保护专治片段边界难题lookbehind捕获组 1 当前缀命中后自动剥离只标记真正目标inside 别名嵌套标记与样式复用让语言包可以组合复用。理解这四个概念你不仅能读懂 prism4cj 的 matchGrammar也掌握了设计任何轻量级语法高亮库的标记化算法思路。【免费下载链接】prism4cj一个轻量的语法高亮库项目地址: https://gitcode.com/Cangjie-TPC/prism4cj创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表