Skip to content

Commit 81be858

Browse files
committed
Add STBareBracketLabelCleanupRule to sanitize bare bracket labels in STMarkdown
- Introduced a new rule to convert bare bracket labels (e.g., `[偏头痛]`, `[咖啡因]`) into plain text, while excluding certain conditions such as valid Markdown links and citation formats. - Updated `STMarkdownInputSanitizer` to include the new rule in the processing pipeline. - Enhanced `STMarkdownBlockLayoutCalculator` with methods for handling standalone colon heading paragraphs and their spacing. - Cleaned up comments and added creation dates in several rendering files for better documentation.
1 parent 9fa7443 commit 81be858

7 files changed

Lines changed: 112 additions & 6 deletions

‎Sources/STMarkdown/Parsing/STMarkdownInputSanitizer.swift‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,86 @@ public struct STDoubleNewlineRule: STMarkdownRule {
230230
}
231231
}
232232

233+
/// LLM 输出中形如 `[偏头痛]`、`[咖啡因]` 的裸括号标签:cmark 将其解析为引用链接,
234+
/// 文档内无对应定义时回退为字面文本,方括号原样渲染。此 rule 将其去括号还原为纯文本。
235+
///
236+
/// 排除条件(不剥离):
237+
/// - 后跟 `(` ——真正的 Markdown 链接
238+
/// - 内容为纯数字 ——citation 编号
239+
/// - 内容匹配 citation:N / webpage:N(大小写不限)——引用标签
240+
/// - 内容含 `[` 或 `]` ——双层括号结构
241+
/// - 位于 fenced code block 内
242+
public struct STBareBracketLabelCleanupRule: STMarkdownRule {
243+
public let name = "STBareBracketLabelCleanupRule"
244+
245+
// 匹配 [xxx],要求 xxx 不含 [ ] \n,且后面不紧跟 (
246+
private static let regex = STMarkdownRegexFactory.compile(
247+
pattern: #"\[([^\[\]\n]+)\](?!\()"#,
248+
owner: "STBareBracketLabelCleanupRule.match"
249+
)
250+
// citation:N 或 webpage:N(可选冒号、可选空格)
251+
private static let citationPattern = STMarkdownRegexFactory.compile(
252+
pattern: #"^(?:citation|webpage)\s*:?\s*\d+$"#,
253+
options: .caseInsensitive,
254+
owner: "STBareBracketLabelCleanupRule.citation"
255+
)
256+
257+
public init() {}
258+
259+
public func shouldApply(to text: String) -> Bool {
260+
text.contains("[")
261+
}
262+
263+
public func apply(to text: String, context: inout STMarkdownPreprocessContext) -> String {
264+
let lines = text.components(separatedBy: "\n")
265+
var result: [String] = []
266+
result.reserveCapacity(lines.count)
267+
var fenceState = STMarkdownCodeFenceState()
268+
269+
for line in lines {
270+
let trimmed = line.trimmingCharacters(in: .whitespaces)
271+
fenceState.ingest(trimmedLine: trimmed)
272+
if fenceState.isInside {
273+
result.append(line)
274+
continue
275+
}
276+
result.append(Self.cleanLine(line))
277+
}
278+
279+
return result.joined(separator: "\n")
280+
}
281+
282+
private static func cleanLine(_ line: String) -> String {
283+
let nsLine = line as NSString
284+
let matches = Self.regex.matches(
285+
in: line,
286+
range: NSRange(location: 0, length: nsLine.length)
287+
)
288+
guard !matches.isEmpty else { return line }
289+
290+
var output = line
291+
for match in matches.reversed() {
292+
guard match.numberOfRanges >= 2,
293+
let labelRange = Range(match.range(at: 1), in: output) else { continue }
294+
let label = String(output[labelRange])
295+
guard !shouldKeep(label) else { continue }
296+
// 用 match.range(at: 0) 对应的当前位置在 output 里重新定位
297+
guard let fullRange = Range(match.range, in: output) else { continue }
298+
output.replaceSubrange(fullRange, with: label)
299+
}
300+
return output
301+
}
302+
303+
private static func shouldKeep(_ label: String) -> Bool {
304+
// 纯数字(citation 编号 standalone)
305+
if label.allSatisfy(\.isNumber) { return true }
306+
// citation:N / webpage:N
307+
let range = NSRange(location: 0, length: (label as NSString).length)
308+
if Self.citationPattern.firstMatch(in: label, range: range) != nil { return true }
309+
return false
310+
}
311+
}
312+
233313
public struct STTableBlankLineNormalizationRule: STMarkdownRule {
234314
public let name = "STTableBlankLineNormalizationRule"
235315

@@ -367,6 +447,7 @@ public struct STMarkdownInputSanitizer: Sendable {
367447
STTableBlankLineNormalizationRule(),
368448
STTableDelimiterNormalizationRule(),
369449
STDoubleNewlineRule(),
450+
STBareBracketLabelCleanupRule(),
370451
]
371452

372453
public let rules: [any STMarkdownRule]

‎Sources/STMarkdown/Rendering/STMarkdownBlockLayoutCalculator.swift‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22
// STMarkdownBlockLayoutCalculator.swift
33
// STBaseProject
44
//
5+
// Created by 寒江孤影 on 2019/03/16.
6+
//
57

68
import UIKit
79

8-
/// 块间距与 `NSAttributedString` 分隔符计算工具。
9-
///
10-
/// 所有方法均为纯函数,无副作用,可静态调用。
11-
/// 宿主(如 `NativeMarkdownView`)不再需要在内部维护等效逻辑。
1210
public enum STMarkdownBlockLayoutCalculator {
1311

14-
// MARK: - Block Spacing
15-
1612
/// 两个相邻块之间的视觉间距(点)。
1713
public static func spacing(
1814
after previousBlock: STMarkdownRenderBlock,
@@ -44,6 +40,9 @@ public enum STMarkdownBlockLayoutCalculator {
4440
case .codeBlock, .table, .mathBlock, .image, .thematicBreak, .details, .rawHTML:
4541
return style.blockSpacing
4642
case .paragraph:
43+
if isStandaloneColonHeadingParagraph(block) {
44+
return pseudoHeadingTopSpacing(style: style)
45+
}
4746
return style.blockSpacing
4847
}
4948
}
@@ -166,4 +165,20 @@ public enum STMarkdownBlockLayoutCalculator {
166165
if let next = nextBlock, isListBlock(next) { return true }
167166
return false
168167
}
168+
169+
/// `paragraph(only-strong, 文本以冒号结尾)` ——LLM 用 `**xxx:**` 单行模拟标题的模式。
170+
public static func isStandaloneColonHeadingParagraph(_ block: STMarkdownRenderBlock) -> Bool {
171+
guard isStandaloneStrongParagraph(block),
172+
case .paragraph(_, let inlines) = block else { return false }
173+
let text = inlineNodesVisibleText(inlines)
174+
return text.hasSuffix(":") || text.hasSuffix(":")
175+
}
176+
177+
/// 伪标题的前导间距:等同 h3 heading top spacing,降级为 blockSpacing * 1.6。
178+
public static func pseudoHeadingTopSpacing(style: STMarkdownStyle) -> CGFloat {
179+
if let topSpacings = style.headingTopSpacing, topSpacings.count >= 3 {
180+
return topSpacings[2] // h3 index
181+
}
182+
return STMarkdownTypography.headingInsets(for: 3).top
183+
}
169184
}

‎Sources/STMarkdown/Rendering/STMarkdownCodeBlockMarkerInfo.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// STMarkdownCodeBlockMarkerInfo.swift
33
// STBaseProject
44
//
5+
// Created by 寒江孤影 on 2019/03/16.
6+
//
57

68
import UIKit
79

‎Sources/STMarkdown/Rendering/STMarkdownInlineCodeBlockRenderer.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// STMarkdownInlineCodeBlockRenderer.swift
33
// STBaseProject
44
//
5+
// Created by 寒江孤影 on 2019/03/16.
6+
//
57

68
import UIKit
79

‎Sources/STMarkdown/Rendering/STMarkdownMermaidAttachmentFactory.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// STMarkdownMermaidAttachmentFactory.swift
33
// STBaseProject
44
//
5+
// Created by 寒江孤影 on 2019/03/16.
6+
//
57

68
import UIKit
79

‎Sources/STMarkdown/UI/STMarkdownTextViewLinkHitTest.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// STMarkdownTextViewLinkHitTest.swift
33
// STBaseProject
44
//
5+
// Created by 寒江孤影 on 2019/04/26.
6+
//
57

68
import UIKit
79

‎Sources/STMarkdown/UI/STMarkdownTextViewMeasure.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// STMarkdownTextViewMeasure.swift
33
// STBaseProject
44
//
5+
// Created by 寒江孤影 on 2019/04/26.
6+
//
57

68
import UIKit
79

0 commit comments

Comments
 (0)