Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
27d57e2
Refine list rendering and enhance table row grouping in STMarkdown
i-stack May 25, 2026
3c8c69b
Add circled number text conversion function to STMarkdownCitationRegex
i-stack May 26, 2026
7a50132
Enhance STMarkdownPlainTextRenderer with additional transformations
i-stack May 26, 2026
438d654
Add streaming line fade mode test to STMarkdownUIViewTests
i-stack May 26, 2026
3aeda65
Update STMarkdownTableView.swift
i-stack May 27, 2026
7c39412
Enhance STMarkdownHighFidelityMathRenderer to improve thread safety a…
i-stack May 27, 2026
a26a605
Enhance STMarkdownHighFidelityMathRenderer to support additional math…
i-stack May 27, 2026
676539b
Refactor STMarkdownMathNormalizer and STMarkdownStreamingTransforms f…
i-stack May 28, 2026
e6fc659
Implement table adjacency handling and CJK character sanitization in …
i-stack May 28, 2026
9e5b21c
Adjust heading insets and block spacing handling in STMarkdown
i-stack May 28, 2026
35cade5
Update STTabBarItemView layout constraints for improved positioning
i-stack May 28, 2026
ce7b66d
Add item layout area insets to STTabBarConfig and update STTabBarItem…
i-stack May 28, 2026
dccdeab
STIconBtn.refineButtonConfiguration 先设置 contentInsets = 18,最后调 super,…
i-stack May 29, 2026
7988479
优化逐字输出
i-stack May 29, 2026
f6f5a61
Enhance STMarkdownMathNormalizer and STMarkdownStreamingTransforms fo…
i-stack May 29, 2026
1857256
Update STBaseProject version to 1.4.0 in README and podspec files
i-stack May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Example/STBaseProjectExampleTests/STMarkdownUIViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,30 @@ final class STMarkdownUIViewTests: XCTestCase {
XCTAssertLessThan(self.foregroundAlpha(in: visible, at: tailRange.location), 0.5)
}

func testStreamingLineFadeModeCreatesMaskAndReportsAnimatingState() {
let style = STMarkdownStyle(
font: .systemFont(ofSize: 16),
textColor: .label,
lineHeight: 22,
kern: 0,
streamFadeInEnabled: true,
streamLineFadeEnabled: true
)
let view = STMarkdownStreamingTextView(style: style, usesTextLayoutManager: false)
view.frame = CGRect(x: 0, y: 0, width: 240, height: 80)
view.layoutIfNeeded()
view.setMarkdown("Hello", animated: false)

var states: [Bool] = []
(view.contentTextView as? STShimmerTextView)?.onAnimationStateChange = { states.append($0) }

view.appendMarkdownFragment(" world", animated: true)

XCTAssertTrue(view.isStreamingAnimationIdle == false)
XCTAssertNotNil(view.contentTextView.layer.mask)
XCTAssertEqual(states.first, true)
}

func testSmartStreamingSecondCommittedFrameUsesIncrementalMergedRenderPath() {
let view = STMarkdownStreamingTextView()
view.tokenFadeDuration = 0
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ STBaseProject 是一个功能强大的 iOS 基础组件库,提供了丰富的
在 `Podfile` 中添加:

```ruby
pod 'STBaseProject', '~> 1.3.0'
pod 'STBaseProject', '~> 1.4.0'
```

然后执行:
Expand All @@ -57,7 +57,7 @@ pod install

```swift
dependencies: [
.package(url: "https://github.com/i-stack/STBaseProject.git", from: "1.3.0")
.package(url: "https://github.com/i-stack/STBaseProject.git", from: "1.4.0")
]
```

Expand Down Expand Up @@ -85,7 +85,7 @@ dependencies: [

```swift
dependencies: [
.package(url: "https://github.com/i-stack/STBaseProject.git", from: "1.3.0")
.package(url: "https://github.com/i-stack/STBaseProject.git", from: "1.4.0")
],
targets: [
.target(
Expand All @@ -105,16 +105,16 @@ targets: [

```ruby
# 默认:仅核心(等价于 default_subspecs)
pod 'STBaseProject', '~> 1.3.0'
pod 'STBaseProject', '~> 1.4.0'

# 核心 + 定位(按需把 STContacts、STMedia、STMarkdown 加入数组即可)
pod 'STBaseProject', '~> 1.3.0', :subspecs => ['STBaseProject', 'STLocation']
pod 'STBaseProject', '~> 1.4.0', :subspecs => ['STBaseProject', 'STLocation']

# 核心 + 多个扩展示例
# pod 'STBaseProject', '~> 1.3.0', :subspecs => ['STBaseProject', 'STLocation', 'STContacts', 'STMedia']
# pod 'STBaseProject', '~> 1.4.0', :subspecs => ['STBaseProject', 'STLocation', 'STContacts', 'STMedia']

# 仅安装某个扩展、不要核心(一般少见;扩展模块不依赖核心时可单独拉取)
# pod 'STBaseProject/STLocation', '~> 1.3.0'
# pod 'STBaseProject/STLocation', '~> 1.4.0'
```

<a id="privacy-permissions"></a>
Expand Down Expand Up @@ -275,13 +275,13 @@ README 仅保留能力概览与模块入口。
仓库内提供自动发布脚本:

```bash
./scripts/release_pod.sh 1.1.6
./scripts/release_pod.sh 1.4.0
```

推荐(自动创建并推送同名 tag):

```bash
./scripts/release_pod.sh 1.1.6 --tag --push-tag
./scripts/release_pod.sh 1.4.0 --tag --push-tag
```

脚本会按顺序执行:
Expand Down
2 changes: 1 addition & 1 deletion STBaseProject.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Pod::Spec.new do |s|
s.name = 'STBaseProject'
s.version = '1.3.0'
s.version = '1.4.0'
s.summary = 'Modular iOS foundation: MVVM bases, networking, security, UIKit, Markdown, localization (SPM & CocoaPods).'
s.description = <<-DESC
STBaseProject is an iOS 16+ modular foundation toolkit distributed via CocoaPods subspecs and Swift Package Manager.
Expand Down
8 changes: 8 additions & 0 deletions Sources/STMarkdown/Core/STMarkdownRegexPatterns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ public enum STMarkdownCitationRegex {
pattern: #"\[[^\]]+\]\([^)]+\)\s*(\[(?i:citation)\s*:?\s*\d+\])"#,
owner: "STMarkdownCitationRegex.linkCitationDeduplicate"
)

/// 将 1–20 的正整数转为对应的 Unicode 带圆圈数字字符(① ② … ⑳)。
/// 超出范围(或非正整数)返回 nil。
public static func circledNumberText(for number: String) -> String? {
guard let value = Int(number), value > 0, value <= 20 else { return nil }
guard let scalar = UnicodeScalar(0x2460 + value - 1) else { return nil }
return String(Character(scalar))
}
}

public enum STMarkdownStreamingRegex {
Expand Down
2 changes: 1 addition & 1 deletion Sources/STMarkdown/Core/STMarkdownTypography.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public enum STMarkdownTypography {
}

public static func headingInsets(for level: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0)
return UIEdgeInsets(top: 18, left: 0, bottom: 18, right: 0)
}

public static func headingParagraphStyle(level: Int, font: UIFont, style: STMarkdownStyle) -> NSMutableParagraphStyle {
Expand Down
78 changes: 75 additions & 3 deletions Sources/STMarkdown/Parsing/STMarkdownMathNormalizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,31 @@ public enum STMarkdownMathNormalizer {
}

if trimmed.hasPrefix("$$") {
let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" }))
let result = consumeDollarMathBlock(from: lines, start: index)
let currentIndex = mathMap.count
mathMap[currentIndex] = result.content.trimmingCharacters(in: .whitespacesAndNewlines)
output.append("")
output.append("{{ST_MATH_BLOCK:\(currentIndex)}}")
output.append(indent + "{{ST_MATH_BLOCK:\(currentIndex)}}")
output.append("")
index = result.nextIndex
continue
}

if trimmed.hasPrefix(#"\["#) {
let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" }))
let result = consumeBracketMathBlock(from: lines, start: index)
let currentIndex = mathMap.count
mathMap[currentIndex] = result.content.trimmingCharacters(in: .whitespacesAndNewlines)
output.append("")
output.append("{{ST_MATH_BLOCK:\(currentIndex)}}")
output.append(indent + "{{ST_MATH_BLOCK:\(currentIndex)}}")
output.append("")
index = result.nextIndex
continue
}

if let environment = environmentName(from: trimmed) {
let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" }))
let result = consumeEnvironmentMathBlock(
from: lines,
start: index,
Expand All @@ -79,7 +82,7 @@ public enum STMarkdownMathNormalizer {
let currentIndex = mathMap.count
mathMap[currentIndex] = result.content.trimmingCharacters(in: .whitespacesAndNewlines)
output.append("")
output.append("{{ST_MATH_BLOCK:\(currentIndex)}}")
output.append(indent + "{{ST_MATH_BLOCK:\(currentIndex)}}")
output.append("")
index = result.nextIndex
continue
Expand Down Expand Up @@ -110,6 +113,10 @@ public enum STMarkdownMathNormalizer {
result = result.replacingOccurrences(of: "⦅ST_LATEX_PAREN_CLOSE⦆", with: #"\)"#)
result = result.replacingOccurrences(of: "⦅ST_LATEX_BRACKET_OPEN⦆", with: #"\["#)
result = result.replacingOccurrences(of: "⦅ST_LATEX_BRACKET_CLOSE⦆", with: #"\]"#)
result = result.replacingOccurrences(of: "⦅ST_MATH_LBRACKET⦆", with: "[")
result = result.replacingOccurrences(of: "⦅ST_MATH_RBRACKET⦆", with: "]")
result = result.replacingOccurrences(of: "⦅ST_MATH_ASTERISK⦆", with: "*")
result = result.replacingOccurrences(of: "⦅ST_MATH_BACKTICK⦆", with: "`")
return result
}

Expand All @@ -119,6 +126,71 @@ public enum STMarkdownMathNormalizer {
result = result.replacingOccurrences(of: #"\)"#, with: "⦅ST_LATEX_PAREN_CLOSE⦆")
result = result.replacingOccurrences(of: #"\["#, with: "⦅ST_LATEX_BRACKET_OPEN⦆")
result = result.replacingOccurrences(of: #"\]"#, with: "⦅ST_LATEX_BRACKET_CLOSE⦆")
result = protectMarkdownCharsInsideMath(in: result)
return result
}

/// 在 `applyInlineSentinels` 已经把 `\(...\)` / `\[...\]` 定界符替换成 sentinel 之后,
/// 把 sentinel 包裹的数学区段内部仍为字面量的 `[`、`]`、`*`、`` ` `` 也换成 sentinel。
///
/// 不处理这一步时,例如 `\(\sum_{k=1}^n k^3 = \left[\frac{n(n+1)}{2}\right]^2\)`,
/// 数学内部的 `[\frac{n(n+1)}{2}\right]` 会被 swift-markdown 识别为
/// shortcut reference link,从而拆掉 Text 节点 + 吃掉中括号,后续
/// `splitInlineMath` 拿不到完整的 `\(...\)` 配对,整段公式退化为纯文本。
///
/// `_` 不需要保护:LaTeX 中 `_` 多为 intraword (`k_n`) 或紧跟标点 (`_{...`),
/// 都不满足 CommonMark left-flanking 条件,无法打开 emphasis。
private static func protectMarkdownCharsInsideMath(in text: String) -> String {
let openParen = "⦅ST_LATEX_PAREN_OPEN⦆"
let closeParen = "⦅ST_LATEX_PAREN_CLOSE⦆"
let openBracket = "⦅ST_LATEX_BRACKET_OPEN⦆"
let closeBracket = "⦅ST_LATEX_BRACKET_CLOSE⦆"

var result = ""
var cursor = text.startIndex

while cursor < text.endIndex {
let tail = text[cursor...]
let parenOpen = tail.range(of: openParen)
let bracketOpen = tail.range(of: openBracket)

let nextOpen: (Range<String.Index>, String)?
switch (parenOpen, bracketOpen) {
case let (p?, b?):
nextOpen = p.lowerBound < b.lowerBound ? (p, closeParen) : (b, closeBracket)
case let (p?, nil):
nextOpen = (p, closeParen)
case let (nil, b?):
nextOpen = (b, closeBracket)
case (nil, nil):
nextOpen = nil
}

guard let (openRange, closingSentinel) = nextOpen else {
result += String(tail)
break
}

result += String(text[cursor..<openRange.upperBound])
cursor = openRange.upperBound

guard let closeRange = text[cursor...].range(of: closingSentinel) else {
// 未闭合的数学块(流式中段):不强行保护,保持现有行为
result += String(text[cursor...])
break
}

let mathContent = text[cursor..<closeRange.lowerBound]
let protected = String(mathContent)
.replacingOccurrences(of: "[", with: "⦅ST_MATH_LBRACKET⦆")
.replacingOccurrences(of: "]", with: "⦅ST_MATH_RBRACKET⦆")
.replacingOccurrences(of: "*", with: "⦅ST_MATH_ASTERISK⦆")
.replacingOccurrences(of: "`", with: "⦅ST_MATH_BACKTICK⦆")
result += protected
result += closingSentinel
cursor = closeRange.upperBound
}

return result
}

Expand Down
Loading
Loading