9 Commits
Author SHA1 Message Date
shenlei e59bad9abf PDF 阅读器新增点读热区、试读墙与阅读进度/书签持久化
宿主的点读书场景需要在 PDF 页面上叠加可点击热区(音频/视频/网页等),并支持
试读限制与阅读进度、书签的本地保存,这些能力此前 RDPDFReaderView 均不提供,
只能由宿主在阅读器之外自行叠加视图,无法跟随 SDK 的缩放、翻页和主题渲染同步。

新增 RDPDFReaderPageInteraction 描述热区(位置、图标、边框/填充样式、点击态、
闪烁与播放边框规则、内嵌视频),由新增的 RDPDFInteractionHotspotView 负责绘制,
SVG 图标经 RDPDFReaderSVGIconLoader(基于 SDWebImage/SDWebImageSVGCoder)异步
解码后回填,避免阻塞主线程;点击事件通过 interactionHandler 回传给宿主,媒体
播放、路由等业务仍留在宿主侧。底部工具栏新增点读提示与连播按钮,状态由宿主驱动。

试读墙对齐 EPUB 侧 RDEPUBReaderTrialPolicy 的设计:以页为限制单位,可读页数在
配置了 trialPolicy 且宿主经 delegate 提供墙视图时生效,原始总页数、缩略图、目录
换算不受影响,只有传给 RDPDFReaderView 的有效页数会追加一页试读墙;翻页、目录
跳转、大纲的上界都统一收敛到可读范围,避免用户绕过热区跳转或朗读高亮跳进未授权页。

阅读进度与书签复用 RDPDFReaderPersistenceStore 已有的标注状态目录,新增
reader-state.json 承载,不需要宿主额外提供数据库表;RDPDFReaderPersistenceStore
同时实现 RDPDFReaderPersistence 协议对接这部分读写。
2026-08-14 12:13:04 +09:00
shenlei 278072aca0 停止跟踪 Demo 的 CocoaPods 产物,改为本地 pod install 生成
ReadViewDemo/Pods 此前整目录签入仓库,但白名单式的 .gitignore 只对 RDEpubReaderView、
RDPDFReaderView 的 Local Podspecs/Target Support Files 和 SnapKit 生效,其余第三方 pod
(DTCoreText、DTFoundation、ZIPFoundation,以及本次新增的 SDWebImage、SDWebImageSVGCoder)
反而因为父目录被显式取消忽略而绕过了全局 gitignore 的 Pods 规则,被当作普通文件跟踪,
导致每次 pod install 产生的构建产物、xcconfig、Pods.xcodeproj 都会出现在 diff 里。

改为直接忽略 ReadViewDemo/Pods 与 ReadViewDemo/Podfile.lock 整体,并 git rm --cached
移除已跟踪的文件(工作区文件不受影响)。此后 Demo 工程的依赖状态完全由 Podfile 决定,
拉取仓库后本地执行 pod install 即可重建,不再需要区分哪些 pod 目录该签入、哪些该忽略。
2026-08-14 12:12:39 +09:00
shenlei 3e60bf1869 修复阅读器资源、朗读与持久化稳定性
阅读器在多会话朗读、加密 EPUB 资源、并发打开同一本书和多 UserDefaults 容器等场景下,存在旧异步结果覆盖新状态、锁屏控制串会话、大型明文资源被误拒绝、解压半成品被复用及标注跨容器混用的风险;同时高亮、书签、搜索和设置面板的空状态与自动化可访问性入口不完整。\n\n本次为朗读会话引入代次和当前 utterance 校验,远程控制改为仅响应当前会话并按自身 token 清理;加密资源 provider 先判定是否实际返回解密数据,明文大资源继续流式读取;解压缓存串行化以避免并发复用未完成目录。书签与高亮迁移至受保护文件,按 UserDefaults 容器隔离命名空间,保留标准容器旧文件兼容并确保新副本成功落盘后才删除历史数据。\n\n同步调整缓存淘汰、FoundationModels 弱链接、搜索/标注/设置面板交互与 UI 测试,并更新 Pod 生成配置及 API、风险文档。已执行 git diff --check 和 ReadViewDemo Debug Simulator 构建,结果为 BUILD SUCCEEDED。
2026-07-30 20:52:21 +09:00
shenlei e01cbc169d 修复 EPUB 与 PDF 阅读器朗读交互
PDF 朗读的逐词回调会在同一页内持续触发高亮,原实现每次都调用翻页入口,使用户在朗读过程中反复看到页面转场。现仅在目标页变化时定位,并对朗读产生的跨页定位禁用动画,保留高亮刷新而不重复翻页。\n\nEPUB/TXT 此前只提供内容源,缺少将朗读进度回写到阅读器的会话层,无法从当前阅读位置开始朗读、随内容跨页及显示临时高亮。新增 EPUB 朗读会话,使用 href 与 UTF-16 偏移恢复位置;文本重排 EPUB 与 TXT 按实际页面定位并高亮,Web EPUB 则按章节安全定位以避免词级回调导致跳页。URL 阅读器同时提供可选会话入口,图片型阅读器保持不可朗读。\n\n已执行 ReadViewDemo 的 Debug Simulator 构建,结果为 BUILD SUCCEEDED。
2026-07-30 18:33:04 +09:00
shenleiandClaude Opus 5 529663829f 修复退出 EPUB 阅读器后宿主侧滑返回手势被改成阅读器的配置
阅读器在 viewWillAppear 中无条件把导航控制器的 interactivePopGestureRecognizer
接管为自己的 delegate 并强制开启,退出时却不做任何还原。宿主如果原本关闭了
侧滑返回,或为它设置过自定义 delegate,从阅读器返回后这些配置就被永久改写,
其它页面的返回手势行为随之变化。阅读器自身接管横向翻页,进入期间开启侧滑返回
也会与翻页手势争抢边缘触摸。

改为进入时记录原有的开关状态与 delegate 并直接关闭该手势,退出时按记录值还原。
记录只在首次 viewWillAppear 时进行,避免阅读器内部多次前后台切换把自己写入的
状态当成宿主原值。

该修复原本以本地补丁形式存在于 readoor 项目的 Vendor 副本中,现回流上游,避免
后续同步 SDK 时反复丢失。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:03:43 +09:00
shenleiandClaude Opus 5 d830311894 修复 PDF 仿真翻页模式下翻页后页面全白
打开普通 PDF 并使用仿真翻页阅读时,除首页外每翻一页都是纯白:没有页面
内容,也没有页面四周的衬底色。横向滚动与竖滑模式不受影响。样张
英汉大词典App需求说明.pdf 上第 2 到第 9 页无一例外。

根因是页面数据回来时找不到对应的视图。UIPageViewController 会通过
viewControllerBefore/After 提前构建相邻页的内容视图,构建时页面描述尚未
就绪,configure 发起异步 readerPage 请求后先返回一张空白页。描述返回后
acceptPageDescriptor 调用 refreshVisiblePage,而它经由
RDPDFReaderView.pageContentView(pageNum:) 定位视图,该方法在仿真翻页下
只检索 nativeCurlPageController.viewControllers,也就是已经上屏的 child。
预建的 child 此时还不在其中,图片因此从未被装配上去;翻页动画结束后又
没有任何路径重新 configure 该视图,白页就固定下来。此前偶尔能看到内容,
是 OCR 完成回调顺带触发了一次刷新,并非稳定行为。

改为按页码刷新该页的全部活动视图:控制器用弱引用表记录已创建的页面视图,
refreshVisiblePage 改名为 refreshPageViews(at:),除已上屏视图外一并装配
尚未上屏的预建视图,pageContentView 创建时即写入 tag 以便按页码匹配。
翻页落定时在 pageNum 回调中补一次装配,使内存告警清掉页面描述的场景也能
重新发起请求并恢复。另外把 image 为空的页面描述改按无效结果处理,走既有
的重试与失败重载入口,不再缓存出一张永远不会重试的白页。

新增 PDFPageTurnBlankPageTests 作为回归:翻页稳定后截屏统计画面中部深色
像素占比,白页为 0,扉页约 0.002,正文页在 0.01 以上,以 0.0005 为判定
阈值,覆盖仿真翻页正翻、仿真回翻和横向滚动三种路径。

验证:iPhone 17 Pro / iOS 26.4.1 模拟器执行 PDFPageTurnBlankPageTests、
PDFAnnotationTests、PDFDrawingModeTests、PageNavigationTests、
DisplayTypeTests 共 23 条用例,22 条通过。修复前仿真翻页第 2 到 9 页墨迹
占比全为 0.0000,修复后与横向滚动模式逐页吻合。唯一失败的
DisplayTypeTests.testDisplayTypeSwitchMidSession 属 EPUB 设置面板既有问题,
将本次改动还原后同样失败。文档补充了 Demo 因链接 FoundationModels 而必须
使用 iOS 26 及以上模拟器的说明。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:52:29 +09:00
shenleiandClaude Opus 5 735c14c824 隐藏 PDF Demo 阅读器中的 AI 助手入口与 TTS 朗读控件
PDF Demo 宿主在阅读界面右上角常驻一个 "AI" 按钮,底部常驻一组 TTS 播放
控件。这两处是示例性质的能力入口,当前不希望在阅读界面上出现。

在宿主控制器上引入 showsAIAssistant 与 showsSpeechControls 两个开关,
viewDidLoad 中据此决定是否创建 AI 按钮、是否装配朗读控件,两者都不再被
加入视图层级。底层的 presentAIAssistant、speechSession 及播放、语速控制
逻辑全部保留,把开关置为 true 即可恢复,避免以后重新接入时再写一遍。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:51:57 +09:00
shenandshen 85cf92161c Merge branch 'dev' of http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK into dev 2026-07-27 21:54:56 +08:00
shenandshen 9392027106 更新阅读器功能与示例 2026-07-27 21:43:13 +08:00
407 changed files with 7800 additions and 45136 deletions
+4 -16
View File
@@ -8,22 +8,10 @@ xcuserdata/
.swift-module-cache/
ReadViewDemo/build/
# This repository checks in the Demo Pods project. Keep generated files for the
# local RDEpubReaderView/RDPDFReaderView pods and vendored SnapKit visible even
# when a global gitignore excludes Pods.
!ReadViewDemo/Pods/
!ReadViewDemo/Pods/Local Podspecs/
!ReadViewDemo/Pods/Local Podspecs/RDEpubReaderView.podspec.json
!ReadViewDemo/Pods/Local Podspecs/RDPDFReaderView.podspec.json
!ReadViewDemo/Pods/Target Support Files/
!ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/
!ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/**
!ReadViewDemo/Pods/Target Support Files/RDPDFReaderView/
!ReadViewDemo/Pods/Target Support Files/RDPDFReaderView/**
!ReadViewDemo/Pods/SnapKit/
!ReadViewDemo/Pods/SnapKit/**
!ReadViewDemo/Pods/Target Support Files/SnapKit/
!ReadViewDemo/Pods/Target Support Files/SnapKit/**
# CocoaPods output for the Demo project is not vendored — everyone runs
# `pod install` locally.
ReadViewDemo/Pods/
ReadViewDemo/Podfile.lock
# macOS system files
.DS_Store
+7 -3
View File
@@ -251,13 +251,17 @@ public protocol RDEPUBReaderPersistence: AnyObject {
}
```
默认实现 `RDEPUBUserDefaultsPersistence` 使用 UserDefaults 存储:
默认实现 `RDEPUBUserDefaultsPersistence` 将阅读位置和全局设置存入 UserDefaults
书签与高亮(可能含书籍正文摘录)存入带文件保护且排除备份的 Application Support 文件。
升级后的首次读取会自动迁移历史 UserDefaults 数据,并仅在新副本成功落盘后删除旧副本。
不同的 `UserDefaults` 容器会自动使用独立的文件命名空间,避免多账号或测试 suite
之间混用同一本书的标注;标准容器保留对旧版未分区文件的只读兼容。
| 数据 | Key 格式 |
|------|----------|
| 阅读位置 | `ssreader.epub.location.{bookID}` |
| 书签 | `ssreader.epub.bookmarks.{bookID}` |
| 高亮 | `ssreader.epub.highlights.{bookID}` |
| 书签 | 受保护文件(历史键:`ssreader.epub.bookmarks.{bookID}` |
| 高亮 | 受保护文件(历史键:`ssreader.epub.highlights.{bookID}` |
| 全局设置 | `ssreader.epub.settings` |
---
+12
View File
@@ -1,6 +1,18 @@
# 代码库风险与关注点
**分析日期:** 2026-05-21
**状态复核:** 2026-07-30(逐条对照源码,结论见下表;正文段落保留原始分析,勿当作现状)
| # | 项目 | 现状 |
|---|------|------|
| 1 | User Script Sandboxing 被关 | **部分处理**:已移除 Podfile 的全局强制覆盖;但显式打成 `YES` 会让 CocoaPods 的 `[CP] Embed Pods Frameworks` rsync 阶段被沙箱拒绝而构建失败(已实测)。要真正开启需先改掉 `use_frameworks!` 动态框架集成方式。 |
| 2 | 外链未做 allowlist | **已修**`RDEPUBReaderController+ExternalLinks.swift` 有 scheme allowlist + 二次确认弹窗 + delegate 否决。 |
| 3 | Zip Slip 未加固 | **已修**`RDEPUBParser+Archive.swift``validatedExtractionDestination` 做 standardized 前缀校验并拒绝 `..`/绝对路径。 |
| 4 | 高亮/选中文本明文写 UserDefaults | **已修**:新增 `RDEPUBProtectedContentStore`,高亮与书签改写 Application Support 下带 file protection 的文件、排除备份,首次读取自动迁移并删除明文;另加 `clearReadingData(for:)` / `clearAllReadingContent()`。 |
| 5 | Scheme handler 整文件读内存 | **原判断有误**>512KB 早已走 `respondWithStreaming` 64KB 分块。本次补上 `maximumInMemoryResourceBytes`(默认 64MB)给走不了流式的加密 provider 分支兜底。 |
| 6 | 分页 WKWebView 可能加载外部资源 | 未复核。 |
| 7 | 解压缓存无限累积 | **已修**`pruneExtractionCache(keeping:)` 先按 `extractionCacheMaximumAge`30 天)再按 `extractionCacheMaximumTotalBytes`(512MB)做 LRU 淘汰;复用时 touch mtime;另有 `clearExtractionCache()`。 |
| 8+ | 其余性能/可维护性项 | 未复核。 |
## 高风险:安全与隐私
@@ -0,0 +1,411 @@
# RDAIReaderView AI 系统设计合同
**文档状态:** Draft 0.1
**最后更新:** 2026-07-25
**目标版本:** RDAIReaderView 1.0
## 1. 系统分类
RDAIReaderView 是一个本地优先的 Retrieval-Augmented Generation 阅读系统,包含:
- 确定性 NLP:语言识别、分句、实体候选、词法检索。
- 本地语义检索:Natural Language embeddings。
- 生成式任务:摘要、书内问答、人物卡片和人物关系。
- 引用约束:生成内容必须映射到 PDF/EPUB 原文。
系统不是通用聊天机器人,也不把模型自身知识作为书籍事实来源。
## 2. 框架选择
### 2.1 主框架
- Apple Natural LanguageiOS 15+ 基础分析和检索。
- Apple Foundation Models:支持设备上的生成式增强。
- Vision:扫描 PDF OCR,继续复用 RDPDFReaderView 现有能力。
### 2.2 选择理由
- 与当前纯 Swift/UIKit/CocoaPods 架构一致。
- 默认设备端处理,不需要新增服务端和书籍上传链路。
- Natural Language 可覆盖不支持 Apple Intelligence 的设备。
- Foundation Models 支持 structured generation 和 tool calling。
- 系统 API 可与现有 PDF/EPUB 定位直接结合。
### 2.3 未选择的首版方案
| 方案 | 首版不采用原因 |
|------|----------------|
| LangChain/LlamaIndex | 主要面向 Python/服务端,增加不必要基础设施 |
| 云端 LLM | 引入内容上传、成本、隐私、版权和网络可用性问题 |
| ONNX Runtime 自带模型 | 需要自行选择、量化、分发和维护语言模型 |
| 自训练 Foundation Models Adapter | 模型版本绑定和 entitlement 增加发布复杂度 |
Core 保留 `RDAIGenerativeProvider`,以后可以增加其他 Provider,不把 Apple 实现写死在公共业务层。
## 3. 模型可用性合同
调用 Foundation Models 前必须检查:
1. API 在当前系统可用。
2. `SystemLanguageModel.default.availability` 为 available。
3. 当前 Locale 被支持。
4. 当前请求未超过并发限制。
5. 当前任务的上下文预算可满足。
不可用原因映射:
| Apple 状态 | RDAI 状态 | UI 行为 |
|------------|-----------|---------|
| deviceNotEligible | `.deviceNotEligible` | 隐藏生成操作,保留基础分析 |
| appleIntelligenceNotEnabled | `.appleIntelligenceNotEnabled` | 说明可在系统设置中开启 |
| modelNotReady | `.modelNotReady` | 展示模型准备中,可稍后重试 |
| unsupported locale | `.languageUnsupported` | 保留检索,关闭生成 |
| unknown | `.unknown` | 通用不可用状态,允许重试 |
禁止通过静态设备型号列表推断可用性,运行时状态是唯一依据。
参考:
- [Foundation Models](https://developer.apple.com/documentation/FoundationModels)
- [SystemLanguageModel](https://developer.apple.com/documentation/FoundationModels/SystemLanguageModel)
- [语言与 Locale 支持](https://developer.apple.com/documentation/foundationmodels/supporting-languages-and-locales-with-foundation-models)
## 4. 输入与上下文策略
### 4.1 唯一事实来源
模型可以使用:
- 本次请求提供的 Passage。
- Passage 的章节标题、页码/资源信息。
- 用户当前问题。
- 非内容性规则,例如输出语言和防剧透范围。
模型不得把训练知识、其他书籍、互联网知识或先前书籍会话作为当前书籍事实来源。
### 4.2 上下文预算
运行时读取模型 `contextSize`,并使用 `tokenCount(for:)` 估算。
初始预算比例:
- 12%instructions 和 schema。
- 5%:用户问题。
- 60%:检索 Passage。
- 17%:输出。
- 6%:安全余量。
若预算不足,按以下顺序处理:
1. 删除低分 Passage。
2. 缩短 Passage 到完整句子边界。
3. 使用预计算的有引用片段摘要。
4. 创建新会话。
5. 仍不足则返回 `contextLimitExceeded`
不得静默截断引用或生成半个结构化对象。
参考:[Managing the context window](https://developer.apple.com/documentation/foundationmodels/managing-the-context-window)
## 5. Prompt 资产管理
Prompt 作为版本化代码资产保存:
```text
FoundationModels/Prompts/
├── summary_v1.swift
├── answer_v1.swift
├── characters_v1.swift
└── relationships_v1.swift
```
每个 Prompt 定义:
- `identifier`
- `version`
- `minimumModelProfile`
- `instructions`
- 输入构造器
- 输出 schema
- 评测集标签
修改 Prompt 必须:
1. 增加版本号。
2. 跑完整离线评测集。
3. 与上一版本对比准确率、拒答、引用和延迟。
4. 更新缓存失效策略。
## 6. 通用 Instructions
所有任务共享以下不可省略规则:
```text
你是书内阅读助手。
只使用提供的原文片段,不使用外部知识补充书中事实。
每个事实性结论必须引用一个或多个有效片段 ID。
证据不足时返回 insufficientEvidence,不猜测。
不得引用允许阅读范围之外的内容。
区分原文明确事实与可能推断。
使用用户当前语言回答。
```
实际实现使用简洁英文或经评测验证的目标语言 instructions;以上文字表达语义合同,不要求逐字使用。
## 7. 结构化输出
### 7.1 摘要 Schema
```swift
@Generable
struct GeneratedSummary {
var overview: String
@Guide(.maximumCount(6))
var points: [GeneratedStatement]
}
@Generable
struct GeneratedStatement {
var text: String
@Guide(.minimumCount(1), .maximumCount(3))
var passageIDs: [String]
}
```
规则:
- `overview` 只能概括已提供 Passage。
- 每个 point 必须有 Passage ID。
- brief 最多 3 点,standard 最多 6 点。
### 7.2 问答 Schema
```swift
@Generable
enum GeneratedAnswerStatus {
case answered
case insufficientEvidence
}
@Generable
struct GeneratedAnswer {
var status: GeneratedAnswerStatus
var answer: String
@Guide(.maximumCount(6))
var statements: [GeneratedStatement]
}
```
`status == insufficientEvidence`
- `statements` 必须为空。
- `answer` 只说明当前已读内容没有足够依据。
- 不推荐用户从互联网获取答案,除非宿主未来明确增加该产品能力。
### 7.3 人物 Schema
```swift
@Generable
struct GeneratedCharacter {
var displayName: String
@Guide(.maximumCount(6))
var aliases: [String]
var description: String
@Guide(.minimumCount(1), .maximumCount(6))
var evidencePassageIDs: [String]
}
```
人物必须在 Passage 中有明确名称或可验证别名。纯代词不能单独创建人物。
### 7.4 关系 Schema
```swift
@Generable
enum GeneratedRelationshipStatus {
case confirmed
case possible
case conflicting
}
@Generable
struct GeneratedRelationship {
var sourceName: String
var targetName: String
var label: String
var status: GeneratedRelationshipStatus
@Guide(.minimumCount(1), .maximumCount(5))
var evidencePassageIDs: [String]
}
```
关系标签应简短,例如“师徒”“同事”“敌对”“亲属”。描述性事件放在人物事件中,不无限创建关系类型。
## 8. 任务设计
### 8.1 章节摘要
输入:
- 当前章节 Passage,或已读范围内的章节片段摘要。
- 目标长度。
- 用户 Locale。
流程:
1. 章节能放入上下文时直接生成。
2. 超长章节分块生成带引用局部摘要。
3. 聚合局部摘要时保留原 Passage ID。
4. 运行引用验证。
禁止只把局部摘要文本作为最终事实来源而丢失原始 Passage ID。
### 8.2 书内问答
流程:
1. 识别问题语言和实体词。
2. Hybrid Retrieval 召回 30 个候选。
3. 重排并过滤到 3-4 个 Passage。
4. 直接把 Passage 放入 Prompt。
5. 生成结构化答案。
6. 验证引用和阅读范围。
7. 无有效事实项时拒答。
首版优先使用代码检索,不默认使用 Tool Calling,这样召回过程更确定、可测试、节省 token。
### 8.3 人物卡片
流程:
1. Natural Language 提供人物候选及出现位置。
2. 按名称和明确别名聚合候选。
3. 检索候选周边 Passage。
4. Foundation Models 生成结构化人物信息。
5. 代码校验所有别名和证据。
6. 低置信别名保持独立候选。
### 8.4 人物关系
采用两阶段:
1. Chunk-level extraction:从局部 Passage 提取关系候选。
2. Document-level merge:按人物 ID、关系标签和时间顺序合并。
合并规则:
- 相同关系 + 相同方向:合并证据。
- 对称关系可由受控词典决定是否双向展示。
- 新证据否定旧关系:状态变为 conflicting。
- 隐含动机、情感和立场默认 possible。
- 任何关系没有有效证据时不入库。
## 9. Tool Calling
仅在单次检索无法回答、且评测证明多轮查找有明显收益时启用。最多提供三个工具:
```text
searchBook(query, scope, limit)
getPassages(ids)
getCharacterEvidence(name, scope, limit)
```
要求:
- 工具只读。
- 工具强制应用文档和已读范围过滤。
- 参数有严格长度和数量上限。
- 返回内容有 token 上限。
- 每个请求最大 Tool 调用次数为 3。
- 检测重复参数调用并终止循环。
- 工具调用和结果 ID进入本地 trace,但不记录原文。
不提供跳页、删除、购买、网络请求等副作用工具。
参考:[Expanding generation with tool calling](https://developer.apple.com/documentation/foundationmodels/expanding-generation-with-tool-calling)
## 10. 确定性后处理
模型输出必须经过:
1. Schema 解码。
2. Passage ID 存在性校验。
3. 阅读范围校验。
4. Quote/Locator 构造。
5. 重复 statement 合并。
6. 空文本和长度校验。
7. 敏感内容与系统错误映射。
8. Artifact 元数据补齐。
模型不能直接构造页码、CFI、CGRect 或数据库 ID;这些字段全部由代码通过 Passage ID解析。
## 11. 安全与产品规则
- 不将 AI 输出表示为作者原话。
- UI 明确标记“AI 生成”。
- possible/conflicting 关系必须视觉区分。
- 用户问题涉及未读内容时,默认拒绝并提示防剧透设置。
- 原文包含违法或敏感内容时,遵循系统模型 guardrails;不能绕过。
- 输入和输出触发系统安全限制时,返回稳定、非技术性的不可生成状态。
- 不要求模型提供医学、法律或金融建议;如果书中包含相关内容,只能解释“书中写了什么”。
发布前必须复核 Apple Foundation Models acceptable use requirements 和最新 App Review Guidelines。
## 12. 关键失败模式
| 失败模式 | 检测 | 处理 |
|----------|------|------|
| 模型编造人物或关系 | Passage ID/名称验证 | 删除无效项,无结果则拒答 |
| 引用存在但不支持结论 | 人工评测 + LLM judge | Prompt 调整,低分结果进入回归集 |
| 同名人物合并 | 别名证据检查 | 保持独立,标记待确认 |
| 未读内容泄漏 | Scope validator | 阻断输出并记录安全计数 |
| OCR 错字导致错误事实 | OCR source 标记和置信策略 | 降低置信度,展示 OCR 来源 |
| Prompt 在系统更新后退化 | 模型版本分桶评测 | 版本化 Prompt 和缓存 |
| 上下文溢出 | tokenCount/contextSize | 重建上下文或分层摘要 |
| Tool 循环 | 调用次数和参数去重 | 终止并降级为现有证据回答 |
| 用户快速重复请求 | request actor + cancellation | 取消旧任务或排队 |
## 13. 评测维度
| 维度 | 定义 | 1.0 门槛 |
|------|------|----------|
| Citation validity | 引用能否恢复到原文 | ≥ 99% |
| Context faithfulness | 事实是否由引用支持 | ≥ 98% |
| Refusal accuracy | 无证据时是否拒答 | ≥ 95% |
| Spoiler safety | 是否只使用允许范围 | 100% |
| Schema validity | 结构化输出是否通过校验 | ≥ 99.5% |
| Character precision | 人物是否真实出现 | ≥ 97% |
| Relationship evidence | 关系是否至少有一条证据 | 100% |
| Alias precision | 自动合并别名是否正确 | ≥ 98% |
| Retrieval recall@5 | 正确证据是否在 Top 5 | ≥ 90% |
| Answer usefulness | 人工 1-5 分平均值 | ≥ 4.0 |
## 14. 评测方法
- 代码指标:Schema、引用 ID、范围、阅读权限、延迟和拒答格式。
- 人工标注:人物、别名、关系、引用支持度、剧透边界。
- LLM judge:只用于语气、完整性和引用支持度的辅助评估;必须先与人工评分校准。
- 生产抽样:只上传宿主允许的匿名数值和用户显式反馈;不上传书籍原文。
评测集和完整方法见 [RDAIReaderView-TEST-PLAN.md](RDAIReaderView-TEST-PLAN.md)。
## 15. 发布检查清单
- [ ] 所有 Prompt 有 identifier 和 version。
- [ ] 每个生成任务使用 `@Generable`
- [ ] 每个事实输出经过 Citation Validator。
- [ ] 防剧透 Scope 在检索前和生成后各检查一次。
- [ ] Foundation Models 不可用路径已真机验证。
- [ ] 上下文预算使用运行时 API 计算。
- [ ] 模型版本变化不会复用旧缓存。
- [ ] 评测集达到所有 1.0 门槛。
- [ ] 日志不包含原文、问题或完整回答。
- [ ] Apple 最新 acceptable use 与审核要求已复核。
+604
View File
@@ -0,0 +1,604 @@
# RDAIReaderView 公共 API 设计
**文档状态:** Proposal 0.1
**最后更新:** 2026-07-25
**目标版本:** RDAIReaderView 1.0
## 1. API 设计原则
- Core 最低支持 iOS 15,不直接依赖 UIKit、PDFKit、DTCoreText 或 FoundationModels。
- 公共模型优先使用值类型,并遵循 `Codable``Sendable``Equatable`
- 文本范围统一使用 UTF-16 偏移,与现有 PDF、EPUB、NSString 和 TTS 范围保持一致。
- Reader Adapter 负责格式转换,Core 不识别 PDF 页视图或 EPUB 排版对象。
- Foundation Models 通过 Provider 协议接入,不能泄漏到基础 API。
- 公开 API 在 1.0 后遵循语义化版本;新增字段必须有解码默认值。
本文中的 Swift 定义是实现合同,允许在不改变语义的前提下调整文件组织和内部实现。
## 2. 标识符与基础范围
```swift
import Foundation
public struct RDAIDocumentIdentifier: RawRepresentable, Codable, Hashable, Sendable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
public struct RDAIResourceIdentifier: RawRepresentable, Codable, Hashable, Sendable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
public struct RDAITextRange: Codable, Hashable, Sendable {
public var location: Int
public var length: Int
public init(location: Int, length: Int) {
self.location = max(0, location)
self.length = max(0, length)
}
public var upperBound: Int { location + length }
}
```
约束:
- `RDAIDocumentIdentifier` 必须与宿主书籍 ID 一致并保持稳定。
- `RDAIResourceIdentifier` 在 PDF 中使用页索引字符串,在 EPUB 中使用规范化 `href`
- 所有文本范围均针对资源原始文本,不针对规范化搜索文本。
## 3. 定位与引用
### 3.1 归一化矩形
Core 使用自定义矩形,避免公共存储格式依赖 UIKit:
```swift
public struct RDAINormalizedRect: Codable, Hashable, Sendable {
public var x: Double
public var y: Double
public var width: Double
public var height: Double
public init(x: Double, y: Double, width: Double, height: Double) {
self.x = x
self.y = y
self.width = width
self.height = height
}
}
```
所有值应限制在 `0...1`。Reader Adapter 负责与 `CGRect` 转换。
### 3.2 格式 Anchor
```swift
public struct RDAIPDFAnchor: Codable, Hashable, Sendable {
public enum TextSource: String, Codable, Sendable {
case native
case ocr
}
public var pageIndex: Int
public var rects: [RDAINormalizedRect]
public var textSource: TextSource
public var readingOrder: Int?
}
public struct RDAIEPUBAnchor: Codable, Hashable, Sendable {
public var href: String
public var cfi: String?
public var rangeCFI: String?
public var progression: Double?
}
public enum RDAIAnchor: Codable, Hashable, Sendable {
case pdf(RDAIPDFAnchor)
case epub(RDAIEPUBAnchor)
}
```
`RDAIAnchor` 必须实现显式 Codable discriminator,例如 `type: "pdf"`,未知类型解码为明确错误,不能误当成其他格式。
### 3.3 通用定位
```swift
public struct RDAILocator: Codable, Hashable, Sendable {
public var documentIdentifier: RDAIDocumentIdentifier
public var resourceIdentifier: RDAIResourceIdentifier
public var textRange: RDAITextRange
public var anchor: RDAIAnchor
public var sourceHash: String
}
public struct RDAICitation: Codable, Hashable, Sendable, Identifiable {
public let id: String
public let passageIdentifier: String
public let quote: String
public let locator: RDAILocator
}
```
引用恢复流程:
1. 校验文档和资源存在。
2. 校验 `sourceHash`
3. 优先按格式 Anchor 恢复。
4. Anchor 失败时使用文本范围和 quote 搜索。
5. 仍失败则返回 `staleCitation`,不得跳转到近似但未验证的位置。
## 4. 文档与资源
```swift
public enum RDAIDocumentFormat: String, Codable, Sendable {
case pdf
case epub
}
public struct RDAIDocumentDescriptor: Codable, Equatable, Sendable {
public let identifier: RDAIDocumentIdentifier
public let title: String
public let format: RDAIDocumentFormat
public let contentRevision: String
}
public struct RDAIResourceDescriptor: Codable, Equatable, Sendable {
public let identifier: RDAIResourceIdentifier
public let title: String?
public let order: Int
public let estimatedUTF16Length: Int?
}
public struct RDAIResourceSnapshot: Sendable {
public let descriptor: RDAIResourceDescriptor
public let sourceText: String
public let sourceHash: String
public let locatorRuns: [RDAILocatorRun]
}
public struct RDAILocatorRun: Sendable {
public let textRange: RDAITextRange
public let anchor: RDAIAnchor
}
```
`contentRevision` 由宿主提供;若宿主没有版本号,Adapter 使用资源哈希汇总生成。
## 5. 内容提供协议
```swift
@MainActor
public protocol RDAIContentProvider: AnyObject {
func aiDocumentDescriptor() -> RDAIDocumentDescriptor
func aiResources() async throws -> [RDAIResourceDescriptor]
func aiResourceSnapshot(
for identifier: RDAIResourceIdentifier
) async throws -> RDAIResourceSnapshot
func aiNavigate(to locator: RDAILocator, animated: Bool) async throws
func aiShowCitationHighlight(_ citation: RDAICitation) async throws
func aiClearCitationHighlight()
}
```
协议标记为 `@MainActor`,因为现有 Reader Controller 和页面缓存均由主线程管理。实现必须只在主线程获取快照引用和 UI 状态;OCR、分块、分析和数据库写入移交后台 actor。
可选读取范围协议:
```swift
public struct RDAIReadScope: Codable, Equatable, Sendable {
public let upperBound: RDAILocator?
public let includesWholeDocument: Bool
}
@MainActor
public protocol RDAIReadScopeProviding: AnyObject {
func aiCurrentReadScope() -> RDAIReadScope
}
```
未实现时,默认只允许当前资源及之前的资源,不能默认整本书。
## 6. Passage 与分析结果
```swift
public enum RDAIPassageKind: String, Codable, Sendable {
case title
case paragraph
case list
case table
case code
case footnote
case unknown
}
public struct RDAIPassage: Codable, Equatable, Sendable, Identifiable {
public let id: String
public let documentIdentifier: RDAIDocumentIdentifier
public let resourceIdentifier: RDAIResourceIdentifier
public let text: String
public let languageCode: String?
public let kind: RDAIPassageKind
public let locator: RDAILocator
public let contentHash: String
public let order: Int
}
public enum RDAIEntityKind: String, Codable, Sendable {
case person
case place
case organization
case other
}
public struct RDAIEntityMention: Codable, Equatable, Sendable, Identifiable {
public let id: String
public let normalizedName: String
public let surfaceText: String
public let kind: RDAIEntityKind
public let confidence: Double
public let locator: RDAILocator
}
```
Natural Language 的标签不是最终人物事实,只是 `RDAIEntityMention` 候选。
## 7. 索引 API
```swift
public enum RDAIIndexState: Equatable, Sendable {
case notStarted
case indexing(completedResources: Int, totalResources: Int)
case paused
case ready
case failed(RDAIError)
}
public struct RDAIIndexOptions: Sendable {
public var scope: RDAIReadScope
public var priorityResource: RDAIResourceIdentifier?
public var allowsEmbeddingAssetDownload: Bool
}
public protocol RDAIIndexing: AnyObject, Sendable {
func prepareIndex(options: RDAIIndexOptions) async throws
func pauseIndexing() async
func resumeIndexing() async
func indexState() async -> RDAIIndexState
func stateUpdates() async -> AsyncStream<RDAIIndexState>
func removeIndex() async throws
}
```
要求:
- `prepareIndex` 幂等。
- 重复调用只能扩大范围或提高优先级,不能创建重复 Job。
- `removeIndex` 删除索引、实体和生成缓存,但不删除原书或用户笔记。
## 8. 能力与可用性
```swift
public enum RDAICapability: String, Codable, Sendable {
case languageAnalysis
case entityExtraction
case lexicalSearch
case semanticSearch
case summarization
case questionAnswering
case characterRelationships
}
public enum RDAIUnavailableReason: Equatable, Sendable {
case operatingSystemUnsupported
case deviceNotEligible
case appleIntelligenceNotEnabled
case modelNotReady
case languageUnsupported(String?)
case embeddingAssetsUnavailable
case providerNotInstalled
case unknown(String)
}
public enum RDAICapabilityAvailability: Equatable, Sendable {
case available
case degraded(reason: RDAIUnavailableReason)
case unavailable(reason: RDAIUnavailableReason)
}
public protocol RDAICapabilityProviding: Sendable {
func availability(
for capability: RDAICapability,
locale: Locale?
) async -> RDAICapabilityAvailability
}
```
UI 只能根据枚举状态展示文案,不能匹配本地化 Error 字符串。
## 9. 检索 API
```swift
public struct RDAIRetrievalOptions: Sendable {
public var maximumResults: Int
public var scope: RDAIReadScope
public var minimumScore: Double
}
public struct RDAIRetrievalMatch: Sendable, Identifiable {
public let id: String
public let passage: RDAIPassage
public let score: Double
public let lexicalScore: Double?
public let semanticScore: Double?
}
public protocol RDAIRetrieving: Sendable {
func retrieve(
query: String,
options: RDAIRetrievalOptions
) async throws -> [RDAIRetrievalMatch]
}
```
检索分数只用于同一索引版本内排序,不承诺跨版本数值稳定。
## 10. 生成结果
### 10.1 摘要
```swift
public enum RDAISummaryLength: String, Codable, Sendable {
case brief
case standard
case detailed
}
public struct RDAISummary: Codable, Sendable {
public let title: String
public let overview: String
public let keyPoints: [RDAISourcedStatement]
public let citations: [RDAICitation]
public let metadata: RDAIGenerationMetadata
}
```
### 10.2 问答
```swift
public enum RDAIAnswerStatus: String, Codable, Sendable {
case answered
case insufficientEvidence
case unsupportedLanguage
case unavailable
}
public struct RDAIAnswer: Codable, Sendable {
public let status: RDAIAnswerStatus
public let text: String
public let statements: [RDAISourcedStatement]
public let citations: [RDAICitation]
public let metadata: RDAIGenerationMetadata
}
public struct RDAISourcedStatement: Codable, Sendable, Identifiable {
public let id: String
public let text: String
public let citationIdentifiers: [String]
}
```
### 10.3 人物关系
```swift
public enum RDAIRelationshipStatus: String, Codable, Sendable {
case confirmed
case possible
case conflicting
}
public struct RDAICharacter: Codable, Sendable, Identifiable {
public let id: String
public let displayName: String
public let aliases: [String]
public let description: String
public let firstAppearance: RDAICitation?
public let evidence: [RDAICitation]
}
public struct RDAIRelationship: Codable, Sendable, Identifiable {
public let id: String
public let sourceCharacterIdentifier: String
public let targetCharacterIdentifier: String
public let label: String
public let status: RDAIRelationshipStatus
public let evidence: [RDAICitation]
}
```
### 10.4 生成元数据
```swift
public struct RDAIGenerationMetadata: Codable, Sendable {
public let providerIdentifier: String
public let modelVersion: String?
public let promptIdentifier: String
public let promptVersion: Int
public let generatedAt: Date
public let scopeHash: String
}
```
元数据用于缓存失效和问题追踪,不向普通用户展示内部 Prompt。
## 11. 高层服务
```swift
public protocol RDAIReaderServicing: AnyObject, Sendable {
func prepare(options: RDAIIndexOptions) async throws
func summarize(
scope: RDAIReadScope,
length: RDAISummaryLength
) async throws -> RDAISummary
func answer(
question: String,
scope: RDAIReadScope
) async throws -> RDAIAnswer
func characters(
scope: RDAIReadScope
) async throws -> [RDAICharacter]
func relationships(
scope: RDAIReadScope
) async throws -> [RDAIRelationship]
func removeAllAIData() async throws
}
```
建议具体实现为 actor。若用户开始新的同类请求,UI 层负责决定取消旧请求或并行;同一 Foundation Models session 不允许并行请求。
## 12. Provider 协议
```swift
public struct RDAIGenerationRequest: Sendable {
public let task: RDAIGenerationTask
public let userText: String?
public let passages: [RDAIPassage]
public let locale: Locale
public let scope: RDAIReadScope
}
public enum RDAIGenerationTask: Sendable {
case summary(RDAISummaryLength)
case answer
case characters
case relationships
}
public protocol RDAIGenerativeProvider: Sendable {
var identifier: String { get }
func availability(locale: Locale) async -> RDAICapabilityAvailability
func generate(_ request: RDAIGenerationRequest) async throws -> RDAIGeneratedArtifact
}
```
`RDAIGeneratedArtifact` 是 Core 内部或受控公共枚举,用于把 Provider 输出转换为第 10 节模型。Provider 不能直接保存结果或操作 Reader UI。
## 13. 错误模型
```swift
public enum RDAIError: Error, Equatable, Sendable {
case invalidDocument
case resourceUnavailable(RDAIResourceIdentifier)
case staleCitation
case indexingFailed(code: String)
case modelUnavailable(RDAIUnavailableReason)
case unsupportedLanguage(String?)
case contextLimitExceeded
case invalidGeneratedStructure
case invalidCitation
case insufficientEvidence
case cancelled
case storageFailure(code: String)
}
```
公共错误不携带原文或数据库底层错误字符串。内部错误映射为稳定 code,并通过本地诊断系统保存脱敏详情。
## 14. PDF Adapter
建议公开:
```swift
public extension RDPDFReaderViewController {
func makeAIContentProvider() -> RDPDFAIContentProvider
func makeAIReaderService(
configuration: RDAIReaderConfiguration = .default
) throws -> RDAIReaderServicing
}
```
映射要求:
- 页面资源 ID 为十进制页索引。
- 文本顺序使用 `RDPDFReaderTextRun.readingOrder`
- `normalizedRects` 转换为 `RDAINormalizedRect`
- 原生文本标记为 `.native`Vision OCR 标记为 `.ocr`
- AI 高亮复用或泛化现有 speech highlight,不同时维护两个相互覆盖的临时层。
## 15. EPUB Adapter
建议公开:
```swift
public extension RDEPUBReaderController {
func makeAIContentProvider() -> RDEPUBAIContentProvider
func makeAIReaderService(
configuration: RDAIReaderConfiguration = .default
) throws -> RDAIReaderServicing
}
```
映射要求:
- 资源 ID 使用 ResourceResolver 规范化后的 `href`
- Passage 范围从章节 attributed content 的原始字符串计算。
- 使用现有 index table 生成 `cfi``rangeCFI`
- 导航复用 `go(to:)`/位置恢复流程。
- 固定版式或无法提取文本的章节返回明确 unavailable,不制造空 Passage。
## 16. TTS 集成
AI 层不依赖 RDSpeechReaderView。宿主可以把摘要或回答转换为临时 `RDSpeechContentProvider`
后续可增加桥接 Pod
```ruby
pod 'RDSpeechReaderView/AI'
```
桥接只负责朗读 AI 结果;Natural Language 的分句与语言识别实现应抽取为共享内部组件,避免同一文本产生不同范围。
## 17. 配置
```swift
public struct RDAIReaderConfiguration: Sendable {
public var spoilerPolicy: RDAISpoilerPolicy
public var maximumRetrievedPassages: Int
public var allowsEmbeddingAssetDownload: Bool
public var storesGeneratedArtifacts: Bool
public var diagnosticsLevel: RDAIDiagnosticsLevel
public static let `default`: RDAIReaderConfiguration
}
```
默认值:
- `spoilerPolicy = .readContentOnly`
- `maximumRetrievedPassages = 4`
- `allowsEmbeddingAssetDownload = false`
- `storesGeneratedArtifacts = true`
- `diagnosticsLevel = .metadataOnly`
## 18. API 演进规则
- 1.0 前可以调整命名,但每次调整同步更新五份设计文档。
- 1.0 后删除或改变语义需要主版本升级。
- Codable 枚举新增 case 时必须实现向后兼容策略。
- 数据库 Schema 版本与 SDK 版本独立。
- Prompt 版本与 SDK 版本独立。
- Reader Adapter 可以增加格式能力,但不能改变 Core Locator 的 UTF-16 语义。
@@ -0,0 +1,431 @@
# RDAIReaderView 架构设计
**文档状态:** Draft 0.1
**最后更新:** 2026-07-25
**目标版本:** RDAIReaderView 1.0
## 1. 架构目标
RDAIReaderView 必须满足四个架构目标:
1. 不改变 RDPDFReaderView、RDEpubReaderView 和 RDSpeechReaderView 的核心职责。
2. iOS 15 用户继续获得稳定阅读、基础 NLP 和 TTSFoundation Models 仅作为可选增强。
3. 所有生成结果都能追溯到稳定原文位置。
4. AI Provider、索引实现和 UI 可替换,公共数据模型保持稳定。
## 2. 总体分层
```text
┌─────────────────────────────────────────────────────────────┐
│ Host App / RDAIReaderViewUI │
│ AI 面板、摘要、问答、人物卡片、关系图、引用跳转 │
├─────────────────────────────────────────────────────────────┤
│ RDAIReaderView │
│ Query Service / Summary Service / Character Service │
├───────────────────────┬─────────────────────────────────────┤
│ NaturalLanguage │ FoundationModels │
│ 分句/语言/实体/检索 │ 结构化生成/工具调用/拒答 │
├───────────────────────┴─────────────────────────────────────┤
│ Index & Storage │
│ Passage / EntityMention / Citation / Artifact / Job │
├───────────────────────┬─────────────────────────────────────┤
│ RDPDFReaderView/AI │ RDEpubReaderView/AI │
│ 页码/范围/矩形/OCR │ href/CFI/范围/章节 │
├───────────────────────┴─────────────────────────────────────┤
│ RDPDFReaderView / RDEpubReaderView / RDSpeechReaderView │
└─────────────────────────────────────────────────────────────┘
```
依赖方向只能从上到下。阅读器核心不能反向依赖 RDAIReaderView。
## 3. CocoaPods 模块设计
建议新增:
```ruby
pod 'RDAIReaderView/Core'
pod 'RDAIReaderView/NaturalLanguage'
pod 'RDAIReaderView/FoundationModels'
pod 'RDAIReaderView/UI'
pod 'RDPDFReaderView/AI'
pod 'RDEpubReaderView/AI'
```
### 3.1 Core
- 最低 iOS 15。
- 只依赖 Foundation、SQLite3/CryptoKit 等系统能力。
- 包含公共协议、数据模型、索引调度、存储和查询编排。
- 不导入 UIKit、PDFKit、DTCoreText 或 FoundationModels。
### 3.2 NaturalLanguage
- 最低 iOS 15。
- 依赖 Core 和 NaturalLanguage。
- 提供语言识别、分句、实体候选、关键词和语义评分。
- iOS 17+ 可选使用 `NLContextualEmbedding`;资源不可用时降级。
### 3.3 FoundationModels
- 源码使用 `@available(iOS 26.0, *)` 隔离。
- 依赖 Core 和系统 FoundationModels。
- 需要支持 Foundation Models 的 Xcode 工具链。
- 不被基础 Pod 默认引入,避免旧工具链客户无法编译。
### 3.4 UI
- 最低 iOS 15。
- 依赖 Core,可选识别 FoundationModels 可用性。
- UI 不直接构造 Prompt,也不直接访问数据库。
### 3.5 Reader Adapters
- `RDPDFReaderView/AI` 依赖 RDAIReaderView/Core。
- `RDEpubReaderView/AI` 依赖 RDAIReaderView/Core。
- Adapter 只负责内容快照、定位转换、跳转和高亮。
## 4. 建议目录
```text
Sources/RDAIReaderView/
├── RDAIReaderView.podspec
├── Core/
│ ├── Contracts/
│ ├── Models/
│ ├── Indexing/
│ ├── Retrieval/
│ ├── Storage/
│ └── Services/
├── NaturalLanguage/
│ ├── Analysis/
│ ├── Embeddings/
│ └── Retrieval/
├── FoundationModels/
│ ├── Availability/
│ ├── Generation/
│ ├── Prompts/
│ ├── Schemas/
│ └── Tools/
├── UI/
│ ├── Assistant/
│ ├── Citations/
│ └── Characters/
└── Tests/
Sources/RDPDFReaderView/AI/
Sources/RDEpubReaderView/AI/
```
## 5. 核心数据流
### 5.1 建立索引
```text
ContentProvider
↓ 读取资源快照
Text Snapshot + Stable Locator
Language Detection
Paragraph/Sentence Chunking
Entity Mentions + Keywords + Embeddings
Transactional Storage
Index Checkpoint
```
每次数据库事务只提交一个资源或一组有界片段。应用退出时,最多重做当前事务,不重做整本书。
### 5.2 问答
```text
User Question
Language / Intent / Spoiler Scope
Hybrid Retrieval
Access + Read-Progress Filter
Context Budget Builder
Foundation Models Structured Generation
Citation Validator
Answer or Evidence-Insufficient Refusal
```
检索由代码执行。首版不让模型自由遍历整本数据库,只有需要多轮查找时才使用受限 Tool Calling。
### 5.3 人物关系
```text
Entity Mentions
Alias Candidate Grouping
Chunk-level Structured Extraction
Evidence Validation
Relationship Merge
confirmed / possible / conflicting
```
代码只能自动合并完全相同的标准化名称和明确别名。代词消解、同名人物合并和隐含关系必须保持低置信度,等待更多证据或用户确认。
## 6. 稳定定位模型
### 6.1 通用定位
`RDAILocator` 包含:
- `documentIdentifier`
- `resourceIdentifier`
- `utf16Range`
- `sourceHash`
- 格式专属 Anchor
文本范围统一使用 UTF-16,与现有 RDSpeechReaderView、NSString 和 EPUB 搜索范围保持一致。
### 6.2 PDF Anchor
```text
pageIndex
normalizedRects
textSource: native / ocr
readingOrder
```
PDF Adapter 从现有 `RDPDFReaderTextRun` 构建连续页文本和 UTF-16 范围。每个 Passage 必须保留与 run 的映射,不能在 AI 层重新拼接后丢失矩形。
扫描 PDF 使用现有 `speechTextRuns(at:)`/OCR 能力,但商用实现应增加独立的 AI OCR 调度入口,避免页面导航取消请求时同时取消后台索引。
### 6.3 EPUB Anchor
```text
normalizedHref
cfi
rangeCFI
rangeAnchor
progressionFallback
```
优先级:
1. `rangeCFI`
2. `cfi + UTF-16 range`
3. `rangeAnchor`
4. `href + progression`
屏幕页码只用于展示,不能作为持久化引用主键。
## 7. 文本快照与分块
### 7.1 不修改原文
索引保存两份文本信息:
- `sourceText`:原始文本,用于引用与范围映射。
- `searchText`:规范化副本,用于检索。
禁止使用规范化文本范围直接驱动阅读器高亮。
### 7.2 分块策略
- 先按资源和章节边界划分。
- 再按段落划分。
- 超长段落使用 `NLTokenizer(unit: .sentence)`
- 中文目标 600-900 字;英文目标 300-600 词。
- 片段之间保留 1-2 句重叠。
- 表格、代码、脚注和标题保留语义类型,避免与正文无差别拼接。
### 7.3 内容哈希
建议使用 SHA-256
```text
documentHash = hash(ordered resource identifiers + resource hashes)
resourceHash = hash(source text + format-specific stable metadata)
passageHash = hash(resource hash + UTF-16 range + source text)
```
书籍更新时按资源哈希增量失效。
## 8. 检索架构
首版采用 Hybrid Retrieval
```text
finalScore =
0.45 * lexicalScore +
0.40 * semanticScore +
0.10 * proximityScore +
0.05 * headingBoost
```
权重是初始值,必须通过评测集调优,不作为永久常量。
检索步骤:
1. 规范化查询并识别语言。
2. 关键词倒排召回 Top 30。
3. 语义相似度重排。
4. 合并高度重叠 Passage。
5. 按已读范围、文档授权和最大上下文过滤。
6. 返回 Top 3-4,并保留评分解释。
若语义模型资源不可用,使用纯词法检索,不阻塞问答入口;UI 可提示结果质量可能降低。
## 9. Foundation Models 编排
### 9.1 Provider 抽象
Core 只依赖 `RDAIGenerativeProvider`。Apple 实现位于 FoundationModels 子模块,未来可增加 Core ML、MLX 或经用户授权的云端实现。
### 9.2 会话策略
- 摘要、问答、人物关系使用不同 instructions 和独立会话。
- 同一会话只处理一个并发请求。
- 用户切换书籍、变更阅读范围或取消时终止任务。
- 达到上下文阈值前主动新建会话,不等待系统抛错。
- 记录 Prompt 版本、模型可用性分类、耗时和 token 数,不记录原文。
### 9.3 上下文预算
默认预算建议:
| 项目 | Token 预算 |
|------|------------|
| Instructions + Schema | 500 |
| 用户问题 | 200 |
| 检索上下文 | 2400 |
| 模型输出 | 700 |
| 安全余量 | 296 |
实际使用 `contextSize``tokenCount(for:)` 动态计算,不写死为 4096。
### 9.4 引用验证
生成后执行确定性校验:
1. Citation ID 必须存在于本次上下文。
2. Citation 必须属于当前文档和允许阅读范围。
3. 引用文本必须能在 Passage 原文中匹配。
4. 每个事实项至少有一个有效引用。
5. 删除无效项后答案为空,则返回 evidence insufficient。
## 10. 存储设计
建议使用 SQLiteSchema 初稿:
```text
documents
resources
passages
passage_fts
embeddings
entity_mentions
entities
entity_aliases
relationships
relationship_evidence
artifacts
index_jobs
schema_metadata
```
关键原则:
- FTS 和关系表通过 Passage ID 关联原文。
- 向量数据按模型标识符、revision 和语言分区。
- 生成结果不覆盖人工编辑内容。
- 每个 Artifact 保存 Prompt/模型/输入哈希。
- 索引表支持按文档级联删除。
## 11. 并发与生命周期
建议采用 Swift Concurrency
- `RDAIIndexCoordinator`actor,管理索引队列和 checkpoint。
- `RDAIStore`actor,串行化数据库写入。
- `RDAIRetriever`:Sendable 服务,可并发读取快照。
- Reader Adapter`@MainActor`,仅提取 UI/阅读器状态和执行跳转。
- Foundation Models Session:由单请求 actor 或服务隔离。
优先级:
1. 用户当前问答所需 Passage。
2. 当前章节。
3. 相邻章节。
4. 已读范围。
5. 其余获准内容。
发生内存警告时:
- 取消低优先级 embedding 任务。
- 卸载 contextual embedding。
- 清理内存 Passage/向量缓存。
- 保留已提交数据库和当前用户请求。
## 12. 可用性与降级
```text
Foundation Models available
├─ 是 → 完整生成能力
└─ 否
├─ Natural Language available → 索引、实体、基础检索
└─ 语言/资源不支持 → 关键词检索与基础分段
```
降级状态是公共 API 的一部分,UI 不根据 Error 字符串猜测原因。
## 13. 安全与隐私
- 默认 Provider 为设备端 Provider。
- Core 不包含网络代码。
- 云端 Provider 若未来增加,必须是单独 Pod,并要求宿主显式配置。
- 日志只记录文档匿名哈希、阶段、耗时、错误类别和计数。
- 禁止记录 Prompt、Passage、用户问题和模型回答全文。
- 导出诊断包前再次脱敏。
- 删除书籍时由宿主调用 `removeDocument`,同时删除索引、关系和生成缓存。
## 14. 可观测性
本地指标:
- 索引耗时、资源数、片段数、失败类别。
- 检索 P50/P95、召回数量和降级模式。
- 生成耗时、取消率、错误类别和引用校验失败率。
- Foundation Models availability 分布。
- 缓存命中率和数据库大小。
商用版本默认只汇总数值。任何远程遥测必须由宿主决定,并遵守其隐私政策。
## 15. 架构决策记录
实施时至少补充以下 ADR
- ADR-001:独立 RDAIReaderView 而非嵌入阅读器核心。
- ADR-002UTF-16 作为跨模块文本范围。
- ADR-003:本地 SQLite 和增量资源哈希。
- ADR-004:检索先行、生成后置、引用强校验。
- ADR-005Foundation Models 可选依赖和运行时降级。
- ADR-006:已读范围作为默认安全边界。
## 16. 已知技术风险
| 风险 | 影响 | 缓解 |
|------|------|------|
| 中文小说实体识别不足 | 人物漏识别或误合并 | 规则候选 + 结构化模型提取 + 证据与置信度 |
| OCR 阅读顺序错误 | 摘要和引用错误 | 保留 readingOrder、双栏测试、允许宿主提供文本 |
| PDF OCR 请求被页面导航取消 | 后台索引不完整 | AI 使用独立调度队列与缓存 |
| EPUB 重排后范围变化 | 引用跳转漂移 | CFI/rangeCFI 优先,文本哈希校验 |
| 系统模型更新 | Prompt 质量回归 | Prompt 版本化、模型版本分层评测 |
| 上下文不足 | 回答遗漏 | 检索压缩、分层摘要、新会话 |
| 同名人物 | 错误关系合并 | 不自动合并低置信候选,保留冲突 |
@@ -0,0 +1,36 @@
# RDAIReaderView Release Checklist
This checklist records the external validation required after the local build
passes. It intentionally contains no book text, prompts, answers or user data.
> Status on 2026-07-25: Apple Intelligence eligible hardware, App Store privacy
> submission and TestFlight rollout access are temporarily unavailable. The
> unchecked physical-device and release gates below are deferred, not passed.
## Local Gates
- [x] Core, NaturalLanguage, FoundationModels and UI compile for iPhoneOS.
- [x] PDF and EPUB adapters compile in the ReadViewDemo workspace.
- [x] PDF/EPUB citations use stable locators and transient highlights.
- [x] SQLite data, FTS and generated artifacts are deleted with a book.
- [ ] Add fixture-backed XCTest coverage for locators, SQLite migration, scope,
citation validation, pause/resume and artifact invalidation.
- [ ] Add UI automation for index, summary, answer, citation jump, clear data
and VoiceOver labels.
## Physical Device Gates
- [ ] On an eligible iOS 26 device, verify each `SystemLanguageModel`
availability state and its UI fallback.
- [ ] Run structured summary and question-answering evaluation fixtures; record
citation validity, faithfulness and spoiler-safety without uploading text.
- [ ] Verify Vision OCR quality and cancellation/retry on scanned PDF samples.
- [ ] Measure index latency, memory, battery and citation jump P95 targets.
## Release Gates
- [ ] Create a feature flag/remote configuration policy owned by the host app.
- [ ] Complete App Store privacy labels, Foundation Models acceptable-use review
and network audit.
- [ ] Run TestFlight rollout 5%, 25%, then 100% with P0/P1 stop conditions.
- [ ] Confirm crash-free and AI quality metrics meet the published thresholds.
+287
View File
@@ -0,0 +1,287 @@
# RDAIReaderView 产品与开发规格
**文档状态:** Draft 0.1
**最后更新:** 2026-07-25
**目标版本:** RDAIReaderView 1.0
**适用工程:** ReadViewSDK
## 1. 文档目的
本文档锁定 RDAIReaderView 首个商用版本的产品范围、系统兼容策略、功能要求、非功能要求和发布门槛。架构、公共 API、生成式 AI 约束和测试方法分别见同目录其他文档。
## 2. 背景与现状
ReadViewSDK 已具备以下基础:
- RDPDFReaderViewPDF 页面、原生文本、Vision OCR、页内文本矩形、跳页与临时朗读高亮。
- RDEpubReaderViewEPUB 章节、`href`、CFI、UTF-16 文本范围、搜索与位置恢复。
- RDSpeechReaderView:基于 Natural Language 的语言识别和分句、系统 TTS、断点续听、后台播放与锁屏控制。
- 最低部署版本为 iOS 15Demo 使用 iOS 15.6。
当前缺少统一的文档语义索引、可追溯引用、书内问答、章节摘要和人物关系能力。
## 3. 产品目标
RDAIReaderView 1.0 的目标是提供一个默认本地运行、可选启用 Apple Foundation Models、可被 PDF 和 EPUB 阅读器复用的智能阅读能力层。
首版必须实现:
1. 对书籍内容建立增量、可恢复的本地索引。
2. 识别语言、句子、人物、地点、组织和关键词。
3. 生成当前章节或已读范围摘要。
4. 回答书内问题,并为事实性陈述提供可跳转的原文引用。
5. 生成人物卡片和带证据的人物关系。
6. Foundation Models 不可用时安全降级,不影响阅读、搜索和 TTS。
7. 所有 AI 结果默认限制在用户已读内容,避免剧透。
## 4. 非目标
RDAIReaderView 1.0 不包含:
- 互联网百科、新闻或通用知识问答。
- 无来源的文学评价、复杂逻辑推理或事实推断。
- 自动改写、续写或批量导出版权书籍内容。
- 云端上传书籍全文。
- 自训练 Foundation Models Adapter。
- 自动替用户发布内容、发送消息或执行购买行为。
- 对 CBZ、漫画图片内容进行视觉剧情理解。
- 对 DRM 内容绕过访问控制或持久化超出宿主授权范围的文本。
上述能力必须在后续版本单独评审产品价值、版权、隐私和审核风险。
## 5. 用户价值与首版场景
### 5.1 阅读回顾
用户重新打开书籍时,可请求:
- 上次阅读内容的 3 句回顾。
- 当前章节摘要。
- 已读部分的关键人物变化。
输入范围默认从最近一个自然章节边界到当前阅读位置,不得包含未读段落。
### 5.2 书内问答
用户可以询问当前书籍,例如:
- “这一章发生了什么?”
- “张三为什么离开?”
- “这里提到的组织是什么?”
系统先检索相关段落,再基于检索结果生成答案。答案中的事实性结论必须关联一个或多个 `RDAICitation`。没有充分证据时必须明确拒答,而不是依赖模型常识补全。
### 5.3 人物与关系
人物卡片包含:
- 标准显示名和已发现的别名。
- 首次出现位置。
- 仅基于已读内容的简短介绍。
- 关键行为及证据。
- 与其他人物的关系边及证据。
关系状态分为:
- `confirmed`:原文明确表达。
- `possible`:模型推断但证据不充分,UI 必须显示“可能”。
- `conflicting`:不同段落给出冲突信息,UI 展示冲突而非自动覆盖。
### 5.4 与阅读器联动
- 点击引用跳转到 PDF 页面或 EPUB CFI。
- 跳转后高亮对应原文范围。
- 摘要、答案和人物卡片可交给 RDSpeechReaderView 朗读。
- 用户调整字体、页面尺寸或翻页方式后,EPUB 引用仍应通过 CFI/文本范围恢复。
## 6. 功能要求
### FR-001 文档导入
- AI 层只能通过 `RDAIContentProvider` 读取宿主已授权的文本快照。
- 不直接读取宿主数据库或下载接口。
- 支持取消导入、增量恢复和内容变更检测。
### FR-002 稳定定位
- PDF:使用文档 ID、页索引、页内 UTF-16 范围和归一化矩形。
- EPUB:使用文档 ID、规范化 `href`、UTF-16 范围和 CFICFI 不可用时才使用 progression 兜底。
- 所有引用必须保存源文本哈希,恢复时验证引用是否仍指向相同内容。
### FR-003 文本分析
- 使用 `NLLanguageRecognizer` 识别资源或段落语言。
- 使用 `NLTokenizer` 切分句子,保持原始 UTF-16 偏移。
- 使用 `NLTagger` 生成人名、地点、组织候选。
- 实体候选必须保留每次出现的原文位置,不能只保存名称。
- Natural Language 不支持或质量不足的语言,降级为字符/标点分段和关键词检索。
### FR-004 分块与索引
- 中文片段建议 600-900 个字符;拉丁文字建议 300-600 词。
- 优先在章节、段落和句子边界切分,不截断组合字符。
- 相邻片段保留 1-2 句重叠,便于跨边界检索。
- 每个片段保存内容哈希、语言、顺序、定位和索引版本。
- 索引任务在后台执行,并按当前章节、相邻章节、其余内容的优先级处理。
### FR-005 检索
- 首版采用关键词/BM25 风格评分与 Natural Language 语义相似度融合。
- 结果必须经过文档 ID、已读范围和访问范围过滤。
- 默认返回 3-4 个片段,最大不超过 5 个。
- 检索结果必须包含分数、匹配原因和稳定定位。
### FR-006 Foundation Models 可用性
- 编译期使用可选子模块,不提高 Core 的 iOS 15 最低版本。
- 运行时检查系统版本、设备资格、Apple Intelligence 开关、模型准备状态和语言支持。
- UI 必须区分 `deviceNotEligible``appleIntelligenceNotEnabled``modelNotReady`、不支持语言和未知错误。
- 不可用时隐藏生成入口或提供明确说明,Natural Language 索引、搜索和 TTS 保持可用。
### FR-007 结构化生成
- 摘要、答案、实体和关系均使用 `@Generable` 结构化输出。
- 输出不得依赖字符串正则解析。
- 每个事实项必须携带检索片段 ID;生成后由代码验证 ID 是否真实存在。
- 无效引用、越权引用或未读范围引用必须删除;删除后答案无证据则转为拒答。
### FR-008 防剧透
- 默认分析范围为“当前位置及之前”。
- 用户主动切换到整本书模式时必须进行一次明确确认。
- 缓存键包含阅读范围;已读摘要不得复用整本书摘要。
- 人物关系图默认只展示已读范围内已出现的人物和关系。
### FR-009 缓存与恢复
- 索引、实体、摘要和问答缓存均存储在应用沙盒。
- 缓存键包含文档内容哈希、索引版本、Prompt 版本、模型版本和阅读范围。
- 内容哈希变化时,失效受影响资源,不强制删除整本书其他有效索引。
- 提供按书删除、删除全部 AI 数据和存储空间统计接口。
### FR-010 用户控制
- 所有长任务支持取消。
- UI 展示索引或生成状态,不伪造确定进度。
- 用户可关闭 AI、清除 AI 缓存、选择“仅本地处理”。
- 生成失败不得阻塞翻页、搜索、标注或 TTS。
## 7. 系统兼容矩阵
| 环境 | 必须提供的能力 |
|------|----------------|
| iOS 15+ | 语言识别、分句、实体候选、关键词索引、基础检索 |
| iOS 17+ | 可选 contextual embedding;资源不存在时允许下载或降级 |
| iOS 26+ 且模型可用 | 摘要、问答、人物关系、结构化笔记 |
| 不支持 Apple Intelligence | Natural Language 能力完整可用,生成式入口降级 |
| 离线 | 已下载模型与本地索引可用;不得要求联网 |
| 扫描 PDF | 使用现有 Vision OCR;OCR 失败的页面明确标记不可分析 |
## 8. 非功能要求
### 8.1 性能
- 索引不得在主线程执行文本分析、向量计算或数据库批量写入。
- 当前章节索引优先完成,目标 P95 不超过 2 秒;具体阈值以目标真机基线校准。
- 10 万中文字的基础索引目标 P95 不超过 30 秒,允许后台增量完成。
- 索引期间阅读页面滚动/翻页帧率不得出现持续性下降。
- 单次 Foundation Models 响应首个可展示结果目标 P95 不超过 5 秒。
- AI 模块空闲 30 秒后应释放 contextual embedding 和不必要的内存缓存。
### 8.2 稳定性
- AI 相关 crash-free session 不低于 99.9%。
- Foundation Models 不可用或生成失败时降级成功率为 100%。
- 强制退出后索引可从最后一个已提交资源恢复。
- 数据库迁移失败时保留原数据库备份,并允许重建索引。
### 8.3 准确性
- 引用定位有效率不低于 99%。
- 事实性陈述有原文支持的比例不低于 98%。
- 无答案问题正确拒答率不低于 95%。
- 人物关系证据覆盖率为 100%。
- 结构化输出通过本地校验的比例不低于 99.5%。
### 8.4 隐私与安全
- 默认不上传书籍文本、查询、摘要、人物关系和阅读历史。
- 日志不得包含原文、用户问题全文或模型完整输出。
- 调试日志必须经过显式编译配置才能包含脱敏片段。
- AI 数据遵循宿主账户登出、删书和清除缓存生命周期。
- 文件保护等级、备份策略和共享容器由宿主配置,SDK 提供明确接口和文档。
### 8.5 可访问性
- 所有 AI 控件支持 VoiceOver、Dynamic Type 和 Reduce Motion。
- 状态变化使用可访问性公告,但不得连续播报索引细节。
- “AI 生成”“可能关系”“无原文证据”等状态不能只靠颜色表达。
## 9. 商用验收门槛
以下条件全部满足后才可发布 1.0
- Core、NaturalLanguage、FoundationModels、PDF Adapter、EPUB Adapter 均有单元测试。
- 关键用户流有 UI 自动化测试。
- 完成至少 100 条人工标注的产品评测集。
- 所有准确性指标达到第 8.3 节门槛。
- 在最低支持系统、主流支持设备和至少两代 Apple Intelligence 设备上完成真机验证。
- 完成模型不可用、未下载、语言不支持、上下文溢出和生成取消测试。
- 完成隐私清单、App Store 隐私申报和 AI 功能说明审核。
- TestFlight 灰度无 P0/P1 缺陷,AI 相关 crash-free session 达标。
- 现有 PDF、EPUB、搜索、标注和 TTS 回归全部通过。
## 10. 实施路线
以下排期按 2 名 iOS 工程师、1 名测试工程师、产品/内容评测兼职参与估算,总周期 12-14 周。单人开发建议按 18-22 周估算。
| 周期 | 阶段 | 主要交付 | 退出条件 |
|------|------|----------|----------|
| 第 1 周 | 合同与工程骨架 | 五份开发文档、Podspec、目录、CI Scheme、ADR | 文档评审通过,空库支持 iOS 15 编译 |
| 第 2-3 周 | Core 与存储 | 公共模型、SQLite Schema、迁移、Job/Checkpoint、删除接口 | 崩溃恢复和增量失效单测通过 |
| 第 4-5 周 | Natural Language | 分句、语言、实体候选、词法/语义检索 | 基础检索评测达标,TTS 范围无漂移 |
| 第 6 周 | PDF Adapter | 原生文本/OCR 快照、Locator、引用跳转和高亮 | PDF 引用恢复率达到门槛 |
| 第 7 周 | EPUB Adapter | href/CFI/rangeCFI 快照、Locator、跳转和高亮 | 重排后引用恢复率达到门槛 |
| 第 8-9 周 | Foundation Models | 可用性、摘要、问答、结构化输出、引用校验 | 不可用降级与 AI 评测通过 |
| 第 10 周 | 人物关系 | 人物、别名、关系、冲突和证据合并 | 人物/关系指标达到门槛 |
| 第 11 周 | 商用 UI | AI 面板、状态、取消、引用、防剧透、无障碍 | 核心 UI 自动化通过 |
| 第 12 周 | 性能与隐私 | 内存、耗电、数据库、日志、清除数据、隐私说明 | 无 P0/P1,性能与隐私门禁通过 |
| 第 13-14 周 | TestFlight 灰度 | 5%→25%→100% 分阶段发布 | Crash-free 和质量反馈持续达标 |
每阶段要求:
- 功能代码、单元测试和文档同一阶段完成。
- 公共 API 变更必须先更新 API 文档。
- Prompt 变更必须增加版本并跑 AI 回归集。
- 阶段退出条件未满足时不得把未验证能力带入下一阶段默认开启。
## 11. 版本范围
### 1.0
- 本地索引。
- 章节摘要。
- 带引用的书内问答。
- 人物卡片和基础人物关系。
- PDF/EPUB 引用跳转。
- 防剧透和完整降级。
### 1.1 候选
- 关系时间线与冲突关系展示。
- 用户划线/笔记参与问答。
- 多本书对照,仅限用户主动选择的本地书籍。
- 可选 Core ML/MLX 或云端 Provider。
### 2.0 候选
- 多模态图片理解。
- 漫画/图文书内容理解。
- 经过独立法律和产品评审的云端增强能力。
## 12. 依赖文档
- [RDAIReaderView-ARCHITECTURE.md](RDAIReaderView-ARCHITECTURE.md)
- [RDAIReaderView-API.md](RDAIReaderView-API.md)
- [RDAIReaderView-AI-SPEC.md](RDAIReaderView-AI-SPEC.md)
- [RDAIReaderView-TEST-PLAN.md](RDAIReaderView-TEST-PLAN.md)
@@ -0,0 +1,473 @@
# RDAIReaderView 商用测试与发布计划
**文档状态:** Draft 0.1
**最后更新:** 2026-07-25
**目标版本:** RDAIReaderView 1.0
## 1. 目标
测试计划验证以下结论:
1. AI 模块不会破坏现有 PDF、EPUB、搜索、标注和 TTS。
2. 索引和引用在书籍更新、分页变化、OCR 和应用中断后仍然可靠。
3. Foundation Models 的输出有证据、可拒答、不剧透并可安全降级。
4. 性能、内存、耗电、隐私和可访问性达到商用要求。
5. Prompt 或系统模型更新后,可以通过可重复评测发现质量回归。
## 2. 测试层级
```text
人工与 TestFlight 验收
UI / 真机系统测试
PDF / EPUB / TTS 集成测试
AI 产品评测与 Prompt 回归
Core / Storage / NLP 单元测试
```
确定性校验优先放在单元测试;非确定性生成质量使用固定数据集、多次运行和人工评审。
## 3. 测试目标与 Target
建议新增:
```text
RDAIReaderViewCoreTests
RDAIReaderViewNaturalLanguageTests
RDAIReaderViewFoundationModelsTests
RDPDFReaderViewAITests
RDEpubReaderViewAITests
ReadViewDemoAIUITests
```
Foundation Models 真机测试与普通 CI 分离,避免不支持模型的 Runner 造成假失败。
## 4. 测试数据
### 4.1 数据来源
只使用:
- 公版书籍。
- 项目拥有测试授权的书籍。
- 团队自行编写的合成文本。
- 经过脱敏、明确允许进入测试仓库的样本。
不得把商业书籍全文或用户内容提交到测试仓库。
### 4.2 数据集组成
首版至少包含:
| 类型 | 最低数量 | 重点 |
|------|----------|------|
| 中文小说章节 | 20 | 多人物、别名、代词、倒叙、否定关系 |
| 英文小说章节 | 10 | 名称大小写、代词、长句 |
| 中文技术/非虚构 | 10 | 术语、组织、事实问答 |
| 英文技术/非虚构 | 10 | 代码块、列表、表格 |
| 原生文本 PDF | 5 本/50 页 | 单栏、双栏、页眉页脚 |
| 扫描 PDF | 5 本/50 页 | OCR 错字、旋转、低清晰度 |
| EPUB | 5 本/30 章 | CFI、脚注、长章节、重排 |
| 无答案问题 | 20 | 拒答 |
| 剧透边界问题 | 20 | 已读/未读范围隔离 |
| 同名或别名关系 | 20 | 实体合并准确性 |
首版产品评测集不少于 100 个 Case。每个 Case 保存输入、允许范围、期望证据、可接受答案要点和禁止行为。
### 4.3 Case 格式
建议使用 JSONL
```json
{
"id": "qa-zh-001",
"task": "questionAnswering",
"documentFixture": "novel-zh-01",
"readScope": {
"resourceOrderUpperBound": 3,
"utf16UpperBound": 8200
},
"input": "林川为什么离开村庄?",
"expectedPassageIDs": ["p-3-18", "p-3-19"],
"requiredFacts": ["受到追捕"],
"forbiddenFacts": ["第四章之后的身份揭示"],
"expectedStatus": "answered"
}
```
测试结果不得把 fixture 原文写入可上传日志。
## 5. Core 单元测试
### 5.1 文本范围
- 空文本、空白文本和超长文本。
- Emoji、组合字符、代理对、中文标点和换行。
- UTF-16 范围与 Swift String Index 双向转换。
- 搜索规范化不能改变 source range。
- Passage 重叠不能产生错误 Locator。
### 5.2 哈希与失效
- 相同内容产生相同哈希。
- 只改变一个章节时只失效该资源。
- Prompt 版本变化只失效相关 Artifact。
- embedding 模型 revision 变化只重建对应向量。
- 阅读范围扩大不复用越界缓存。
### 5.3 存储与迁移
- 首次建库、重复打开和并发读取。
- 每个 Schema 版本向下一版本迁移。
- 迁移中断后恢复或回滚。
- 数据库损坏时保留诊断并可安全重建。
- 按书删除和删除全部数据。
- 登出/删书回调后无孤立 Passage、向量或 Artifact。
### 5.4 索引调度
- 当前章节优先。
- 暂停、恢复、取消和重复 prepare。
- 应用强制退出后从 checkpoint 恢复。
- 内存警告取消低优先级任务。
- 内容 Provider 释放后任务安全失败,不野指针或永久等待。
## 6. Natural Language 测试
### 6.1 语言与分句
- 中文、英文、中英混排。
- 无标点长段落。
- 缩写、小数、网址和引号。
- 句子范围与 RDSpeechReaderView 结果一致。
- 不支持语言的降级分块。
### 6.2 实体
按任务统计 Precision、Recall、F1
```text
precision = 正确识别实体 / 所有识别实体
recall = 正确识别实体 / 所有标注实体
F1 = 2 * precision * recall / (precision + recall)
```
首版门槛:
- 人物候选 Precision ≥ 0.95。
- 人物候选 Recall ≥ 0.85。
- 地点/组织作为辅助信息,F1 ≥ 0.80。
人物候选宁可少召回,也不能大量创建虚假人物。
### 6.3 检索
- 关键词命中、同义表达、别名和错别字。
- Top 5 包含正确证据的 Recall@5 ≥ 0.90。
- 无 embedding 资源时词法检索仍可返回结果。
- 已读范围外 Passage 召回数必须为 0。
- 相同输入和索引版本的词法结果顺序稳定。
## 7. PDF Adapter 测试
- 原生文本 run 按 `readingOrder` 正确拼接。
- 双栏页面不会按几何坐标错误穿插。
- run 间换行计入 UTF-16 范围。
- Passage 范围映射回正确 normalized rects。
- characterRects 存在/缺失均可高亮。
- OCR 与原生文本来源正确标记。
- OCR 缓存命中、失败、取消和重试。
- 页面缓存裁剪后 Citation 仍可重新加载。
- 跳转后目标页和高亮正确。
- 旋转、横屏、竖滑、双页模式下高亮位置正确。
- 页面导航取消 OCR 时,AI 索引任务不应永久丢失。
PDF 引用定位有效率必须 ≥ 99%。
## 8. EPUB Adapter 测试
- `href` 规范化一致。
- UTF-16 range、rangeAnchor、CFI 和 rangeCFI 互相映射。
- 改字号、行距、边距、字体和横竖屏后 Citation 可恢复。
- 长章节按需加载时可提取目标资源。
- 脚注、列表、图片替代文本和代码块类型正确。
- EPUB 更新导致 source hash 变化时旧 Citation 标记 stale。
- CFI 缺失时 progression 只作兜底。
- 引用跳转复用标准位置恢复流程。
- 固定版式无文本章节返回明确不可分析状态。
EPUB 重排后引用定位有效率必须 ≥ 99%。
## 9. Foundation Models 产品评测
### 9.1 运行方式
- 每个 Case 至少运行 3 次,避免偶然结果掩盖问题。
- 按系统模型版本、系统语言和设备分桶。
- Prompt 新版本同时运行旧版和新版,生成差异报告。
- 代码校验先执行,再进入人工/模型评分。
### 9.2 摘要 Rubric
| 分数 | 标准 |
|------|------|
| 5 | 覆盖关键事件,全部有证据,无未读信息,表述简洁 |
| 3 | 基本正确但遗漏一项重要内容,或引用不够精确 |
| 1 | 包含无证据事实、重大误解或剧透 |
发布门槛:
- 平均分 ≥ 4.0。
- 任一剧透 Case 失败即阻断发布。
- 无证据事实比例 ≤ 2%。
### 9.3 问答 Rubric
检查:
- 是否回答用户问题。
- 每个事实是否被引用支持。
- 是否遗漏关键反证。
- 证据不足时是否拒答。
- 是否泄漏未读内容。
发布门槛:
- Context faithfulness ≥ 98%。
- 无答案正确拒答率 ≥ 95%。
- Citation validity ≥ 99%。
- Spoiler safety = 100%。
### 9.4 人物与关系 Rubric
检查:
- 人物真实出现。
- 别名有明确证据。
- 同名人物未误合并。
- 关系方向正确。
- `confirmed``possible` 分类合理。
- 冲突关系没有被静默覆盖。
发布门槛:
- Character precision ≥ 97%。
- Alias merge precision ≥ 98%。
- 关系证据覆盖率 = 100%。
- 无证据关系数 = 0。
### 9.5 LLM Judge
LLM Judge 只作为辅助:
- 使用固定 Rubric,不使用泛化“是否有帮助”问题。
- 先对至少 30 个 Case 与人工评分校准。
- Spearman/Pearson 相关性低于 0.7 时不得作为发布门禁。
- Judge 分歧、低分和边界 Case 必须人工复核。
- 若使用云端 Judge,测试原文必须为可上传的公版或合成数据。
## 10. 可用性与错误测试
覆盖:
- iOS 15/17Foundation Models 模块不参与运行。
- iOS 26+ 不支持设备。
- Apple Intelligence 未开启。
- 模型正在下载或未准备。
- 当前语言不支持。
- context limit exceeded。
- guardrail 拒绝输入或输出。
- 生成中取消、切书、关闭页面、进入后台。
- 同一 session 并发请求。
- 低存储空间和数据库写入失败。
每种情况必须产生稳定枚举状态,并保持阅读器可操作。
## 11. UI 与可访问性
### 11.1 UI 自动化
- 打开/关闭 AI 面板。
- 当前章节索引状态。
- 发起问题、取消和重试。
- 点击引用并跳转高亮。
- 仅已读范围默认开启。
- 整本书模式确认。
- Foundation Models 不可用状态。
- 清除本书和全部 AI 数据。
- AI 结果交给 TTS 朗读。
### 11.2 可访问性
- VoiceOver 顺序和标签。
- Dynamic Type 最大辅助字号。
- Reduce Motion。
- 深色模式和高对比度。
- `possible`/`conflicting` 不只依赖颜色。
- 加载、取消和失败状态有可访问性公告。
## 12. 性能与资源测试
目标设备至少覆盖:
- 最低性能 iOS 15 支持设备。
- iOS 17 中档设备。
- 一台首代支持 Apple Intelligence 的设备。
- 一台当前系统的高性能设备。
基准:
| 指标 | 1.0 目标 |
|------|----------|
| 当前章节基础索引 P95 | ≤ 2 秒 |
| 10 万中文字基础索引 P95 | ≤ 30 秒 |
| 本地检索 P95 | ≤ 300 ms |
| 生成首个可展示结果 P95 | ≤ 5 秒 |
| 引用跳转 P95 | ≤ 500 ms,不含未缓存页面加载 |
| 索引峰值额外内存 | 目标 ≤ 150 MB,按真机基线确认 |
| 空闲内存释放 | 30 秒内释放 embedding 和大文本缓存 |
| AI crash-free session | ≥ 99.9% |
测试同时记录:
- CPU time。
- 主线程卡顿。
- thermal state。
- 电量变化。
- 数据库大小/万字。
- embedding 资源加载耗时。
- 缓存命中率。
性能基线变化超过 15% 时 CI 或发布报告必须提示。
## 13. 隐私与安全测试
- 网络抓包确认默认实现不上传书籍或问题。
- 搜索日志、控制台日志和 crash breadcrumbs 不含原文。
- 清除 AI 数据后数据库、缓存和临时文件均删除。
- 删除书籍和退出账户触发相同清除路径。
- Scope 过滤在检索前和生成后均执行。
- 构造伪造 Passage ID,确认 Citation Validator 拒绝。
- 构造路径、超长查询和大量 Tool 参数,确认边界限制。
- Tool Calling 最大次数和重复调用检测生效。
- 数据库迁移和诊断包不泄漏原文。
## 14. 回归测试
每次合入必须运行:
- Core 单元测试。
- Natural Language 确定性测试。
- PDF/EPUB Adapter fixture 测试。
- 现有 PDF、EPUB 和 TTS 编译。
- `git diff --check` 和公共 API 兼容检查。
每日或候选发布运行:
- UI smoke。
- 完整 AI 评测集。
- 大书索引性能。
- Foundation Models 真机矩阵。
- 现有 `ReadViewDemoUITests` 回归。
## 15. CI 建议
```text
PR:
lint/diff-check
Core unit tests
NLP unit tests
Adapter tests
build iOS 15 target
build iOS 26 FoundationModels target
Nightly:
Full reader UI regression
AI deterministic evals
Performance fixtures
Release candidate:
Foundation Models physical-device eval
Human review sample
Privacy/network audit
Migration matrix
```
设备模型评测结果应保存以下元数据:
- OS 版本。
- 模型版本或可识别 profile。
- Prompt identifier/version。
- fixture version。
- SDK commit。
- 各指标与失败 Case ID。
不得保存生产用户原文。
## 16. 缺陷分级
### P0
- 泄漏书籍内容或阅读历史。
- 绕过已读范围造成剧透。
- 删除用户原书、标注或笔记。
- 大面积崩溃或数据库不可恢复损坏。
### P1
- 无引用或错误引用的事实作为确定答案展示。
- 人物关系大面积误合并。
- Foundation Models 不可用导致阅读器不可用。
- 引用跳转到错误章节/页面。
### P2
- 摘要遗漏、检索质量下降、局部 UI 或性能问题。
- 可重试且不影响阅读主流程的生成失败。
发布时 P0/P1 必须为 0;P2 必须有明确接受记录和后续版本计划。
## 17. TestFlight 灰度
### 阶段 A:内部
- 团队和测试设备。
- 至少 7 天。
- 完整日志仅限脱敏元数据。
### 阶段 B5%
- 只开启摘要和问答。
- 观察 crash-free、取消率、拒答率、引用点击成功率。
- 人物关系仍受远程/本地 feature flag 控制。
### 阶段 C25%
- 开启人物卡片。
- 关系图只对达到索引完整度的书籍开放。
- 至少稳定 7 天。
### 阶段 D100%
- 所有发布门禁持续达标。
- 保留快速关闭 Foundation Models 功能的配置,但关闭后基础阅读和 NLP 正常。
## 18. 最终发布门禁
- [ ] 五份开发文档与实现一致。
- [ ] 公共 API 兼容检查通过。
- [ ] Core/NLP/Adapter 单元和集成测试通过。
- [ ] 现有 Reader 与 TTS 回归通过。
- [ ] 100+ AI Case 全量运行并达到指标。
- [ ] Foundation Models 支持与不支持路径均完成真机测试。
- [ ] PDF/EPUB Citation validity ≥ 99%。
- [ ] Context faithfulness ≥ 98%。
- [ ] Spoiler safety = 100%。
- [ ] AI crash-free session ≥ 99.9%。
- [ ] 隐私和网络审计通过。
- [ ] 无 P0/P1 缺陷。
- [ ] TestFlight 灰度指标稳定。
+7
View File
@@ -34,6 +34,7 @@
| LocationPersistenceTests.swift | 阅读位置持久化 |
| MetadataParseBenchmarkTests.swift | 元数据解析性能基准 |
| PageNavigationTests.swift | 页面导航 |
| PDFPageTurnBlankPageTests.swift | PDF 翻页后白页回归(截屏墨迹占比判定) |
| ReaderAnnotationTests.swift | 标注基础功能 |
| ReaderAnnotationExtendedTests.swift | 标注扩展功能 |
| ReaderOpenCloseTests.swift | 阅读器打开/关闭 |
@@ -72,6 +73,12 @@ cd ReadViewDemo && pod install
(依赖入口:`ReadViewDemo/Podfile`
### 模拟器要求
Demo 链接了 `FoundationModels.framework`(AI 助手示例入口)。该框架自 iOS 26 起提供,
在 iOS 18 及更早的模拟器上 App 会在启动时因 `Library not loaded` 直接崩溃,测试表现为
卡在 Launch 阶段。请选择 iOS 26 及以上的模拟器运行 UI 测试。
### 运行 UI 测试
```bash
+8 -2
View File
@@ -1,6 +1,6 @@
# ReadViewSDK 文档索引
> 最后更新:2026-06-18
> 最后更新:2026-07-25
---
@@ -25,7 +25,7 @@
| 文档 | 说明 |
|------|------|
| [CONCERNS.md](CONCERNS.md) | 代码库风险与关注点:12 项安全/性能/可维护性风险 |
| [CONCERNS.md](CONCERNS.md) | 代码库风险与关注点:12 项安全/性能/可维护性风险(顶部有 2026-07-30 状态复核表) |
| [TESTING.md](TESTING.md) | 测试基础设施:UI 测试文件清单、运行方式、覆盖率 |
| [CONVENTIONS.md](CONVENTIONS.md) | 编码规范:命名约定、代码风格、导入规范、错误处理 |
@@ -33,6 +33,12 @@
| 文档 | 说明 |
|------|------|
| [RDAIReaderView/RDAIReaderView-SPEC.md](RDAIReaderView/RDAIReaderView-SPEC.md) | AI 阅读能力产品范围、兼容策略、实施路线与商用验收门槛 |
| [RDAIReaderView/RDAIReaderView-ARCHITECTURE.md](RDAIReaderView/RDAIReaderView-ARCHITECTURE.md) | RDAIReaderView 模块、索引、检索、存储、引用和降级架构 |
| [RDAIReaderView/RDAIReaderView-API.md](RDAIReaderView/RDAIReaderView-API.md) | RDAIReaderView 公共模型、Provider、Reader Adapter 和服务 API 合同 |
| [RDAIReaderView/RDAIReaderView-AI-SPEC.md](RDAIReaderView/RDAIReaderView-AI-SPEC.md) | Natural Language 与 Foundation Models 的 Prompt、结构化输出、安全和评测合同 |
| [RDAIReaderView/RDAIReaderView-TEST-PLAN.md](RDAIReaderView/RDAIReaderView-TEST-PLAN.md) | AI 商用测试集、质量指标、真机矩阵、CI、灰度与发布门禁 |
| [RDAIReaderView/RDAIReaderView-RELEASE-CHECKLIST.md](RDAIReaderView/RDAIReaderView-RELEASE-CHECKLIST.md) | 本地构建后所需的真机、隐私、TestFlight 与发布验证清单 |
| [TYPESetter_PIPELINE.md](TYPESetter_PIPELINE.md) | Typesetter 排版管线详解:HTML 规范化、语义标记注入、CFI 标记、样式合成为、字体规范化、片段标记 |
| [CHAPTER_RUNTIME.md](CHAPTER_RUNTIME.md) | 章节运行时详解:按需加载、章节窗口协调、页图管理、磁盘缓存、后台补全 |
| [CFI_SUBSYSTEM.md](CFI_SUBSYSTEM.md) | CFI 子系统详解:EPUB CFI 解析、生成、序列化、范围、恢复引擎 |
+16 -1
View File
@@ -3,14 +3,29 @@ platform :ios, '15.6'
target 'ReadViewDemo' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'RDSpeechReaderView/AI', :path => '../Sources/RDSpeechReaderView'
pod 'RDAIReaderView/NaturalLanguage', :path => '../Sources/RDAIReaderView'
pod 'RDAIReaderView/FoundationModels', :path => '../Sources/RDAIReaderView'
pod 'RDAIReaderView/UI', :path => '../Sources/RDAIReaderView'
pod 'RDEpubReaderView', :path => '../Sources/RDEpubReaderView'
pod 'RDEpubReaderView/Speech', :path => '../Sources/RDEpubReaderView'
pod 'RDEpubReaderView/AI', :path => '../Sources/RDEpubReaderView'
pod 'RDPDFReaderView', :path => '../Sources/RDPDFReaderView'
pod 'RDPDFReaderView/Speech', :path => '../Sources/RDPDFReaderView'
pod 'RDPDFReaderView/AI', :path => '../Sources/RDPDFReaderView'
end
post_install do |installer|
# 不再由本 Podfile 全局关闭 ENABLE_USER_SCRIPT_SANDBOXING——之前的写法会把
# 宿主工程一起强制改成 NO,属于我们主动降级安全基线。现在交回工程默认值。
#
# 注意:本工程用 use_frameworks!CocoaPods 生成的 [CP] Embed Pods Frameworks
# 脚本阶段用 rsync 往 .app/Frameworks 拷贝产物;一旦把该设置显式打成 YES,
# 这一步会被沙箱拒绝(file-write-unlink / utimensat: Operation not permitted
# 而整个构建失败——已实测确认。真要打开必须先改掉动态框架集成方式
# (静态库 / SPM),别只翻这个开关。
apply_settings = lambda do |config|
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
-55
View File
@@ -1,55 +0,0 @@
PODS:
- DTCoreText (1.6.28):
- DTCoreText/Core (= 1.6.28)
- DTFoundation/Core (~> 1.7.19)
- DTFoundation/DTAnimatedGIF (~> 1.7.19)
- DTFoundation/DTHTMLParser (~> 1.7.19)
- DTFoundation/UIKit (~> 1.7.19)
- DTCoreText/Core (1.6.28):
- DTFoundation/Core (~> 1.7.19)
- DTFoundation/DTAnimatedGIF (~> 1.7.19)
- DTFoundation/DTHTMLParser (~> 1.7.19)
- DTFoundation/UIKit (~> 1.7.19)
- DTFoundation/Core (1.7.19)
- DTFoundation/DTAnimatedGIF (1.7.19)
- DTFoundation/DTHTMLParser (1.7.19):
- DTFoundation/Core
- DTFoundation/UIKit (1.7.19):
- DTFoundation/Core
- RDEpubReaderView (0.0.2):
- DTCoreText (~> 1.6)
- SnapKit (~> 5.7)
- ZIPFoundation (~> 0.9)
- RDPDFReaderView (0.0.1):
- SnapKit (~> 5.7)
- SnapKit (5.7.1)
- ZIPFoundation (0.9.20)
DEPENDENCIES:
- RDEpubReaderView (from `../Sources/RDEpubReaderView`)
- RDPDFReaderView (from `../Sources/RDPDFReaderView`)
SPEC REPOS:
trunk:
- DTCoreText
- DTFoundation
- SnapKit
- ZIPFoundation
EXTERNAL SOURCES:
RDEpubReaderView:
:path: "../Sources/RDEpubReaderView"
RDPDFReaderView:
:path: "../Sources/RDPDFReaderView"
SPEC CHECKSUMS:
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
RDEpubReaderView: 5a09a7856f04b6f2f8610aa6fd965d35cadb8e17
RDPDFReaderView: f7c2d0ea7c129aa4ab15ae7f9daad2974573d749
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: c39db27dbc9c6a96a33f22e78ce1b22dcd48d75f
COCOAPODS: 1.16.2
-12
View File
@@ -1,12 +0,0 @@
//
// CTLineUtils.h
// DTCoreText
//
// Created by Oleksandr Deundiak on 7/15/15.
// Copyright 2015. All rights reserved.
//
#import <CoreText/CoreText.h>
BOOL areLinesEqual(CTLineRef line1, CTLineRef line2);
CFIndex getTruncationIndex(CTLineRef line, CTLineRef trunc);
-102
View File
@@ -1,102 +0,0 @@
//
// CTLineUtils.m
// DTCoreText
//
// Created by Oleksandr Deundiak on 7/15/15.
// Copyright 2015. All rights reserved.
//
#import "CTLineUtils.h"
BOOL areLinesEqual(CTLineRef line1, CTLineRef line2)
{
if(line1 == nil || line2 == nil) {
return NO;
}
CFArrayRef glyphRuns1 = CTLineGetGlyphRuns(line1);
CFArrayRef glyphRuns2 = CTLineGetGlyphRuns(line2);
CFIndex runCount1 = CFArrayGetCount(glyphRuns1), runCount2 = CFArrayGetCount(glyphRuns2);
if (runCount1 != runCount2)
return NO;
for (CFIndex i = 0; i < runCount1; i++)
{
CTRunRef run1 = CFArrayGetValueAtIndex(glyphRuns1, i);
CTRunRef run2 = CFArrayGetValueAtIndex(glyphRuns2, i);
CFIndex countInRun1 = CTRunGetGlyphCount(run1), countInRun2 = CTRunGetGlyphCount(run2);
if (countInRun1 != countInRun2)
return NO;
const CGGlyph* constGlyphs1 = CTRunGetGlyphsPtr(run1);
CGGlyph* glyphs1 = NULL;
if (constGlyphs1 == NULL)
{
glyphs1 = (CGGlyph*)malloc(countInRun1*sizeof(CGGlyph));
CTRunGetGlyphs(run1, CFRangeMake(0, countInRun1), glyphs1);
constGlyphs1 = glyphs1;
}
const CGGlyph* constGlyphs2 = CTRunGetGlyphsPtr(run2);
CGGlyph* glyphs2 = NULL;
if (constGlyphs2 == NULL)
{
glyphs2 = (CGGlyph*)malloc(countInRun2*sizeof(CGGlyph));
CTRunGetGlyphs(run2, CFRangeMake(0, countInRun2), glyphs2);
constGlyphs2 = glyphs2;
}
BOOL result = YES;
for (CFIndex j = 0; j < countInRun1; j++)
{
if (constGlyphs1[j] != constGlyphs2[j])
{
result = NO;
break;
}
}
if (glyphs1 != NULL)
free(glyphs1);
if (glyphs2 != NULL)
free(glyphs2);
if (!result)
return NO;
}
return YES;
}
CFIndex getTruncationIndex(CTLineRef line, CTLineRef trunc)
{
if (line == nil || trunc == nil) return 0;
CFIndex truncCount = CFArrayGetCount(CTLineGetGlyphRuns(trunc));
CFArrayRef lineRuns = CTLineGetGlyphRuns(line);
CFIndex lineRunsCount = CFArrayGetCount(lineRuns);
CFIndex index = lineRunsCount - truncCount - 1;
// If the index is negative, CFArrayGetValueAtIndex will crash on iOS 10 beta.
// We will just return 0 because on iOS 9, CFArrayGetValueAtIndex would have
// returned nil anyways and the return truncation index would be 0.
// Apple might have enabled an assert that only appears in the iOS 10 beta
// release, but we will just avoid passing invalid arguments just to be safe.
if (index < 0)
{
return 0;
}
else
{
CTRunRef lineLastRun = CFArrayGetValueAtIndex(lineRuns, index);
CFRange lastRunRange = CTRunGetStringRange(lineLastRun);
return lastRunRange.location = lastRunRange.length;
}
}
@@ -1,38 +0,0 @@
//
// DTAccessibilityElement.h
// DTCoreText
//
// Created by Austen Green on 3/13/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <UIKit/UIKit.h>
/**
A UIAccessibilityElement subclass that automatically converts its local accessibilityFrame to screen coordinates.
*/
@interface DTAccessibilityElement : UIAccessibilityElement
/**
The frame for the accessibility element in terms of the receiver's superview.
*/
@property (nonatomic, assign) CGRect localCoordinateAccessibilityFrame;
/**
The point for activating accessibility events in terms of the receiver's superview.
*/
@property (nonatomic, assign) CGPoint localCoordinateAccessibilityActivationPoint;
/**
The designated initializer. This class should be initialized with a UIView as its accessibility container.
@param parentView The logical superview for the onscreen element the receiver represents.
@returns Returns an initialized DTAccessibilityElement */
- (id)initWithParentView:(UIView *)parentView;
@end
#endif
@@ -1,56 +0,0 @@
//
// DTAccessibilityElement.m
// DTCoreText
//
// Created by Austen Green on 3/13/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTAccessibilityElement.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <DTFoundation/DTWeakSupport.h>
static const CGPoint DTAccessibilityElementNullActivationPoint = {CGFLOAT_MAX, CGFLOAT_MAX};
@interface DTAccessibilityElement()
@property (nonatomic, DT_WEAK_PROPERTY) UIView *parentView;
@end
@implementation DTAccessibilityElement
- (id)initWithParentView:(UIView *)parentView
{
self = [super initWithAccessibilityContainer:parentView];
if (self)
{
_parentView = parentView;
_localCoordinateAccessibilityActivationPoint = DTAccessibilityElementNullActivationPoint;
}
return self;
}
- (CGRect)accessibilityFrame
{
CGRect frame = self.localCoordinateAccessibilityFrame;
frame = [self.parentView.window convertRect:frame fromView:self.parentView];
return frame;
}
- (CGPoint)accessibilityActivationPoint
{
CGPoint point = self.localCoordinateAccessibilityActivationPoint;
if (CGPointEqualToPoint(point, DTAccessibilityElementNullActivationPoint))
{
point = [super accessibilityActivationPoint];
}
point = [self.parentView.window convertPoint:point fromView:self.parentView];
return point;
}
@end
#endif
@@ -1,61 +0,0 @@
//
// DTAccessibilityViewProxy.h
// DTCoreText
//
// Created by Austen Green on 5/6/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTAccessibilityElement.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTTextAttachment.h"
#import <DTFoundation/DTWeakSupport.h>
@protocol DTAccessibilityViewProxyDelegate;
/**
UIView proxy for DTAttributedTextContentView custom subviews for text attachments.
*/
@interface DTAccessibilityViewProxy : NSObject
/**
The delegate for the proxy
*/
@property (nonatomic, DT_WEAK_PROPERTY, readonly) id<DTAccessibilityViewProxyDelegate> delegate;
/**
The text attachment represented by the proxy
*/
@property (nonatomic, strong, readonly) DTTextAttachment *textAttachment;
/**
Creates a text attachment proxy for use with the VoiceOver system.
@param textAttachment The <DTTextAttachment> that will be represented by a view.
@param delegate An object conforming to <DTAccessibilityViewProxyDelegate> that will provide a view when needed by the proxy.
@returns A new proxy object
*/
- (id)initWithTextAttachment:(DTTextAttachment *)textAttachment delegate:(id<DTAccessibilityViewProxyDelegate>)delegate;
@end
/**
Protocol to provide custom views for accessibility elements representing a DTTextAttachment.
*/
@protocol DTAccessibilityViewProxyDelegate
@required
/**
Provides a view for an attachment, e.g. an imageView for images
@param attachment The <DTTextAttachment> that the requested view should represent
@param proxy The frame that the view should use to fit on top of the space reserved for the attachment.
@returns The sender requesting the view.
*/
- (UIView *)viewForTextAttachment:(DTTextAttachment *)attachment proxy:(DTAccessibilityViewProxy *)proxy;
@end
#endif
@@ -1,62 +0,0 @@
//
// DTAccessibilityViewProxy.m
// DTCoreText
//
// Created by Austen Green on 5/6/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTAccessibilityViewProxy.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
@implementation DTAccessibilityViewProxy
- (id)initWithTextAttachment:(DTTextAttachment *)textAttachment delegate:(id<DTAccessibilityViewProxyDelegate>)delegate
{
_textAttachment = textAttachment;
_delegate = delegate;
return self;
}
- (UIView *)proxiedView
{
return [self.delegate viewForTextAttachment:self.textAttachment proxy:self];
}
- (Class)class
{
Class aClass = [[self proxiedView] class];
if (!aClass)
aClass = [DTAccessibilityViewProxy class];
return aClass;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
NSMethodSignature *signature = [UIView instanceMethodSignatureForSelector:sel];
return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
UIView *view = [self proxiedView];
[invocation invokeWithTarget:view];
}
- (BOOL)isEqual:(id)object
{
return [[self proxiedView] isEqual:object];
}
- (NSUInteger)hash
{
return [[self proxiedView] hash];
}
@end
#endif
@@ -1,21 +0,0 @@
//
// DTHTMLElementA.h
// DTCoreText
//
// Created by Oliver Drobnik on 21.03.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTHTMLElement.h"
/**
Specialized subclass of <DTHTMLElement> that represents a hyperlink.
*/
@interface DTAnchorHTMLElement : DTHTMLElement
/**
Foreground text color of the receiver when highlighted
*/
@property (nonatomic, strong) DTColor *highlightedTextColor;
@end
@@ -1,54 +0,0 @@
//
// DTHTMLElementA.m
// DTCoreText
//
// Created by Oliver Drobnik on 21.03.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTAnchorHTMLElement.h"
#import "DTColorFunctions.h"
@implementation DTAnchorHTMLElement
{
DTColor *_highlightedTextColor;
}
- (void)applyStyleDictionary:(NSDictionary *)styles
{
[super applyStyleDictionary:styles];
// get highlight color from a:active pseudo-selector
NSString *activeColor = [styles objectForKey:@"active:color"];
if (activeColor)
{
self.highlightedTextColor = DTColorCreateWithHTMLName(activeColor);
}
}
- (NSAttributedString *)attributedString
{
// super returns a mutable attributed string
NSMutableAttributedString *mutableAttributedString = (NSMutableAttributedString *)[super attributedString];
if (_highlightedTextColor)
{
NSRange range = NSMakeRange(0, [mutableAttributedString length]);
// this additional attribute keeps the highlight color
[mutableAttributedString addAttribute:DTLinkHighlightColorAttribute value:(id)_highlightedTextColor range:range];
// we need to set the text color via the graphics context
[mutableAttributedString addAttribute:(id)kCTForegroundColorFromContextAttributeName value:[NSNumber numberWithBool:YES] range:range];
}
return mutableAttributedString;
}
#pragma mark - Properties
@synthesize highlightedTextColor = _highlightedTextColor;
@end
@@ -1,44 +0,0 @@
//
// DTAttributedLabel.h
// DTCoreText
//
// Created by Brian Kenny on 1/17/13.
// Copyright (c) 2013 Cocoanetics.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTAttributedTextContentView.h"
/**
A Rich Text replacement for `UILabel`. It inherits from <DTAttributedTextContentView> and as such you can also set the delegate to provide custom subviews i.e. for images or hyperlinks.
Contrary to DTAttributedTextContentView the intrinsicContentSize is only as wide as the text content. To shrink the DTAttributedLabel to that call -sizeToFit.
*/
@interface DTAttributedLabel : DTAttributedTextContentView
/**
@name Setting Attributes
*/
/**
The number of lines to display in the receiver
*/
@property(nonatomic, assign) NSInteger numberOfLines;
/**
The line break mode of the receiver
*/
@property(nonatomic, assign) NSLineBreakMode lineBreakMode;
/**
The string to append to the visible string in case a truncation occurs
*/
@property(nonatomic, strong) NSAttributedString *truncationString;
@end
#endif
@@ -1,125 +0,0 @@
//
// DTAttributedLabel.m
// DTCoreText
//
// Created by Brian Kenny on 1/17/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTAttributedLabel.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTCoreTextLayoutFrame.h"
#import <QuartzCore/QuartzCore.h>
@implementation DTAttributedLabel
+ (Class)layerClass
{
// most likely the label will be less than a screen size and so we don't want any tiling behavior
return [CALayer class];
}
- (void) setupAttributedLabel
{
// we want to relayout the text if height or width change
self.relayoutMask = DTAttributedTextContentViewRelayoutOnHeightChanged | DTAttributedTextContentViewRelayoutOnWidthChanged;
self.layoutFrameHeightIsConstrainedByBounds = YES; // height is not flexible
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self setupAttributedLabel];
}
return self;
}
- (id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self != nil)
{
[self setupAttributedLabel];
}
return self;
}
- (void) awakeFromNib
{
[super awakeFromNib];
[self setupAttributedLabel];
}
#pragma mark - Sizing
- (CGSize)intrinsicContentSize
{
if (!self.layoutFrame) // creates new layout frame if possible
{
return CGSizeMake(-1, -1); // UIViewNoIntrinsicMetric as of iOS 6
}
// we have a layout frame and from this we get the needed size
CGSize intrisicContentSize = [_layoutFrame intrinsicContentFrame].size;
return CGSizeMake(intrisicContentSize.width + _edgeInsets.left + _edgeInsets.right,
intrisicContentSize.height + _edgeInsets.top + _edgeInsets.bottom);
}
#pragma mark - Properties
- (NSInteger)numberOfLines
{
return _numberOfLines;
}
- (void)setNumberOfLines:(NSInteger)numberOfLines
{
if (numberOfLines != _numberOfLines)
{
_numberOfLines = numberOfLines;
[self relayoutText];
}
}
- (NSLineBreakMode)lineBreakMode
{
return _lineBreakMode;
}
- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode
{
if (lineBreakMode != _lineBreakMode)
{
_lineBreakMode = lineBreakMode;
[self relayoutText];
}
}
- (NSAttributedString*)truncationString
{
return _truncationString;
}
- (void)setTruncationString:(NSAttributedString *)truncationString
{
if (![truncationString isEqualToAttributedString:_truncationString])
{
_truncationString = truncationString;
[self relayoutText];
}
}
@end
#endif
@@ -1,93 +0,0 @@
//
// DTAttributedTextCell.h
// DTCoreText
//
// Created by Oliver Drobnik on 8/4/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <UIKit/UIKit.h>
#import "DTAttributedTextContentView.h"
#import <DTFoundation/DTWeakSupport.h>
/**
This class represents a tableview cell that contains an attributed text as its content.
*/
@interface DTAttributedTextCell : UITableViewCell
/**
@name Creating Cells
*/
/**
Creates a tableview cell with a given reuse identifier.
@param reuseIdentifier The reuse identifier to use for the cell
@returns A prepared cell
*/
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier;
/**
@name Setting Attributed Content
*/
/**
The attributed string content of the receiver
*/
@property (nonatomic, strong) NSAttributedString *attributedString;
/**
A delegate implementing DTAttributedTextContentViewDelegate to provide custom subviews for images and links.
*/
@property (nonatomic, DT_WEAK_PROPERTY) IBOutlet id <DTAttributedTextContentViewDelegate> textDelegate;
/**
This method allows to set HTML text directly as content of the receiver.
This will be converted to an attributed string.
@param html The HTML string to set as the receiver's text content
*/
- (void)setHTMLString:(NSString *)html;
/**
This method allows to set HTML text directly as content of the receiver.
This will be converted to an attributed string.
@param html The HTML string to set as the receiver's text content
@param options The options used for rendering the HTML
*/
- (void) setHTMLString:(NSString *)html options:(NSDictionary*) options;
/**
@name Getting Information
*/
/**
Determines the row height that is needed in a specific table view to show the entire text content.
The table view is necessary because from this the method can know the style. Also the accessory type needs to be set before calling this method because this reduces the available space.
@note This value is only useful for table views with variable row height.
@param tableView The table view to determine the height for.
*/
- (CGFloat)requiredRowHeightInTableView:(UITableView *)tableView;
/**
Determines whether the cells built-in contentView is allowed to dictate the size available for text. If active then attributedTextContextView's height always matches the cell height.
Set this to `YES` for use in fixed row height table views, leave it `NO` for flexible row height table views.
*/
@property (nonatomic, assign) BOOL hasFixedRowHeight;
/**
The attributed text content view that the receiver uses to display the attributed text content.
*/
@property (nonatomic, readonly) DTAttributedTextContentView *attributedTextContextView;
@end
#endif
@@ -1,276 +0,0 @@
//
// DTAttributedTextCell.m
// DTCoreText
//
// Created by Oliver Drobnik on 8/4/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTAttributedTextCell.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTCoreText.h"
#import "DTCSSStylesheet.h"
#import <DTFoundation/DTLog.h>
@implementation DTAttributedTextCell
{
DTAttributedTextContentView *_attributedTextContextView;
DT_WEAK_VARIABLE id <DTAttributedTextContentViewDelegate> _textDelegate;
NSUInteger _htmlHash; // preserved hash to avoid relayouting for same HTML
BOOL _hasFixedRowHeight;
DT_WEAK_VARIABLE UITableView *_containingTableView;
}
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
if (self)
{
// content view created lazily
}
return self;
}
- (void)dealloc
{
_textDelegate = nil;
_containingTableView = nil;
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (!self.superview)
{
return;
}
if (_hasFixedRowHeight)
{
self.attributedTextContextView.frame = self.contentView.bounds;
}
else
{
CGFloat neededContentHeight = [self requiredRowHeightInTableView:_containingTableView];
// after the first call here the content view size is correct
CGRect frame = CGRectMake(0, 0, self.contentView.bounds.size.width, neededContentHeight);
self.attributedTextContextView.frame = frame;
}
}
- (UITableView *)_findContainingTableView
{
UIView *tableView = self.superview;
while (tableView)
{
if ([tableView isKindOfClass:[UITableView class]])
{
return (UITableView *)tableView;
}
tableView = tableView.superview;
}
return nil;
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
_containingTableView = [self _findContainingTableView];
// on < iOS 7 we need to make the background translucent to avoid artifacts at rounded edges
if (_containingTableView.style == UITableViewStyleGrouped)
{
if (NSFoundationVersionNumber < DTNSFoundationVersionNumber_iOS_7_0)
{
_attributedTextContextView.backgroundColor = [UIColor clearColor];
}
}
}
// http://stackoverflow.com/questions/4708085/how-to-determine-margin-of-a-grouped-uitableview-or-better-how-to-set-it/4872199#4872199
- (CGFloat)_groupedCellMarginWithTableWidth:(CGFloat)tableViewWidth
{
CGFloat marginWidth;
if(tableViewWidth > 20)
{
if(tableViewWidth < 400 || [UIDevice currentDevice].userInterfaceIdiom==UIUserInterfaceIdiomPhone)
{
marginWidth = 10;
}
else
{
marginWidth = MAX(31.f, MIN(45.f, tableViewWidth*0.06f));
}
}
else
{
marginWidth = tableViewWidth - 10;
}
return marginWidth;
}
- (CGFloat)requiredRowHeightInTableView:(UITableView *)tableView
{
if (_hasFixedRowHeight)
{
DTLogWarning(@"You are calling %s even though the cell is configured with fixed row height", (const char *)__PRETTY_FUNCTION__);
}
BOOL ios6Style = (NSFoundationVersionNumber < DTNSFoundationVersionNumber_iOS_7_0);
CGFloat contentWidth = tableView.frame.size.width;
// reduce width for grouped table views
if (ios6Style && tableView.style == UITableViewStyleGrouped)
{
contentWidth -= [self _groupedCellMarginWithTableWidth:contentWidth] * 2;
}
// reduce width for accessories
switch (self.accessoryType)
{
case UITableViewCellAccessoryDisclosureIndicator:
{
contentWidth -= ios6Style ? 20.0f : 10.0f + 8.0f + 15.0f;
break;
}
case UITableViewCellAccessoryCheckmark:
{
contentWidth -= ios6Style ? 20.0f : 10.0f + 14.0f + 15.0f;
break;
}
#if TARGET_OS_IOS
case UITableViewCellAccessoryDetailDisclosureButton:
{
contentWidth -= ios6Style ? 33.0f : 10.0f + 42.0f + 15.0f;
break;
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
case UITableViewCellAccessoryDetailButton:
{
contentWidth -= 10.0f + 22.0f + 15.0f;
break;
}
#endif
#endif
case UITableViewCellAccessoryNone:
{
break;
}
default:
{
DTLogWarning(@"AccessoryType %d not implemented on %@", self.accessoryType, NSStringFromClass([self class]));
break;
}
}
CGSize neededSize = [self.attributedTextContextView suggestedFrameSizeToFitEntireStringConstraintedToWidth:contentWidth];
// note: non-integer row heights caused trouble < iOS 5.0
return neededSize.height;
}
#pragma mark Properties
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)setHTMLString:(NSString *)html
{
[self setHTMLString:html options:nil];
}
- (void) setHTMLString:(NSString *)html options:(NSDictionary*) options {
NSUInteger newHash = [html hash];
if (newHash == _htmlHash)
{
return;
}
_htmlHash = newHash;
NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:data options:options documentAttributes:NULL];
self.attributedString = string;
[self setNeedsLayout];
}
- (void)setAttributedString:(NSAttributedString *)attributedString
{
// passthrough
self.attributedTextContextView.attributedString = attributedString;
}
- (NSAttributedString *)attributedString
{
// passthrough
return _attributedTextContextView.attributedString;
}
- (DTAttributedTextContentView *)attributedTextContextView
{
if (!_attributedTextContextView)
{
// don't know size because there's no string in it
_attributedTextContextView = [[DTAttributedTextContentView alloc] initWithFrame:self.contentView.bounds];
_attributedTextContextView.edgeInsets = UIEdgeInsetsMake(5, 5, 5, 5);
_attributedTextContextView.layoutFrameHeightIsConstrainedByBounds = _hasFixedRowHeight;
_attributedTextContextView.delegate = _textDelegate;
[self.contentView addSubview:_attributedTextContextView];
}
return _attributedTextContextView;
}
- (void)setHasFixedRowHeight:(BOOL)hasFixedRowHeight
{
if (_hasFixedRowHeight != hasFixedRowHeight)
{
_hasFixedRowHeight = hasFixedRowHeight;
[self setNeedsLayout];
}
}
- (void)setTextDelegate:(id)textDelegate
{
_textDelegate = textDelegate;
_attributedTextContextView.delegate = _textDelegate;
}
@synthesize attributedTextContextView = _attributedTextContextView;
@synthesize hasFixedRowHeight = _hasFixedRowHeight;
@synthesize textDelegate = _textDelegate;
@end
#endif
@@ -1,370 +0,0 @@
//
// DTAttributedTextContentView.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/9/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTCoreTextLayoutFrame.h"
#import <DTFoundation/DTWeakSupport.h>
@class DTAttributedTextContentView;
@class DTCoreTextLayoutFrame;
@class DTTextBlock;
@class DTCoreTextLayouter;
@class DTTextAttachment;
/**
notification that gets sent as soon as the receiver has done a layout pass
*/
extern NSString * const DTAttributedTextContentViewDidFinishLayoutNotification;
/**
Protocol to provide custom views for elements in an DTAttributedTextContentView. Also the delegate gets notified once the text view has been drawn.
*/
@protocol DTAttributedTextContentViewDelegate <NSObject>
@optional
/**
@name Notifications
*/
/**
Called before a layout frame or a part of it is drawn. The text delegate can draw contents that goes under the text in this method.
@param attributedTextContentView The content view that will be drawing a layout frame
@param layoutFrame The layout frame that will be drawn for
@param context The graphics context that will drawn into
*/
- (void)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView willDrawLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame inContext:(CGContextRef)context;
/**
Called after a layout frame or a part of it is drawn. The text delegate can draw contents that goes over the text in this method.
@param attributedTextContentView The content view that drew a layout frame
@param layoutFrame The layout frame that was drawn for
@param context The graphics context that was drawn into
*/
- (void)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView didDrawLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame inContext:(CGContextRef)context;
/**
Called before the text belonging to a text block is drawn.
This gives the developer an opportunity to draw a custom background below a text block.
@param attributedTextContentView The content view that drew a layout frame
@param textBlock The text block
@param frame The frame within the content view's coordinate system that will be drawn into
@param context The graphics context that will be drawn into
@param layoutFrame The layout frame that will be drawn for
@returns `YES` is the standard fill of the text block should be drawn, `NO` if it should not
*/
- (BOOL)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView shouldDrawBackgroundForTextBlock:(DTTextBlock *)textBlock frame:(CGRect)frame context:(CGContextRef)context forLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame;
/**
@name Providing Custom Views for Content
*/
/**
Provide custom view for an attachment, e.g. an imageView for images
@param attributedTextContentView The content view asking for a custom view
@param attachment The <DTTextAttachment> that this view should represent
@param frame The frame that the view should use to fit on top of the space reserved for the attachment
@returns The view that should represent the given attachment
*/
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttachment:(DTTextAttachment *)attachment frame:(CGRect)frame;
/**
Provide button to be placed over links, the identifier is used to link multiple parts of the same A tag
@param attributedTextContentView The content view asking for a custom view
@param url The `NSURL` of the hyperlink
@param identifier An identifier that uniquely identifies the hyperlink within the document
@param frame The frame that the view should use to fit on top of the space reserved for the attachment
@returns The view that should represent the given hyperlink
*/
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForLink:(NSURL *)url identifier:(NSString *)identifier frame:(CGRect)frame;
/**
Provide generic views for all attachments.
This is only called if the more specific delegate methods are not implemented.
@param attributedTextContentView The content view asking for a custom view
@param string The attributed sub-string containing this element
@param frame The frame that the view should use to fit on top of the space reserved for the attachment
@returns The view that should represent the given hyperlink or text attachment
@see attributedTextContentView:viewForAttachment:frame: and attributedTextContentView:viewForAttachment:frame:
*/
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttributedString:(NSAttributedString *)string frame:(CGRect)frame;
@end
enum {
DTAttributedTextContentViewRelayoutNever = 0,
DTAttributedTextContentViewRelayoutOnWidthChanged = 1 << 0,
DTAttributedTextContentViewRelayoutOnHeightChanged = 1 << 1,
};
typedef NSUInteger DTAttributedTextContentViewRelayoutMask;
/**
Attributed Text Content Views display attributed strings generated by DTHTMLAttributedStringBuilder. They can display images and hyperlinks inline or optionally place custom subviews (which get provided via the <delegate> in the appropriate places. By itself content views do not scroll, for that there is the `UIScrollView` subclass <DTAttributedTextView>.
Generally you have two options to providing content:
- set the attributed string
- set a layout frame
The first you would normally use, the second you would use if you are layouting a larger text and then simply want to display individual parts (e.g. pages from an e-book) in a content view.
DTAttributedTextContentView is designed to be used as the content view inside a DTAttributedTextView and thus sizes its intrinsicContentSize always to be the same as the width of the set frame. Use DTAttributedLabel if you don't require scrolling behavior.
*/
@interface DTAttributedTextContentView : UIView
{
NSAttributedString *_attributedString;
DTCoreTextLayoutFrame *_layoutFrame;
UIEdgeInsets _edgeInsets;
NSMutableDictionary *customViewsForAttachmentsIndex;
BOOL _flexibleHeight;
// for layoutFrame
NSInteger _numberOfLines;
NSLineBreakMode _lineBreakMode;
NSAttributedString *_truncationString;
}
/**
@name Sizing
*/
/**
Calculates the suggested frame size that would fit the entire <attributedString> with a maximum width.
This does a full layout pass that is cached in <DTCoreTextLayouter>. If you specify a frame that fits the result from this method then the resulting layoutFrame is reused.
Since this obeys the <edgeInsets> you have to add these to the final frame size.
@param width The maximum width to layout for
@returns The suggested frame size
*/
- (CGSize)suggestedFrameSizeToFitEntireStringConstraintedToWidth:(CGFloat)width; // obeys the edge insets
/**
The size of contents of the receiver. This is possibly used by auto-layout, but also for example if you want to get the size of the receiver necessary for a scroll view
This method is defined as of iOS 6, but to support earlier OS versions
*/
- (CGSize)intrinsicContentSize;
/**
Whether the receiver calculates layout limited to the view bounds.
If set to `YES` then the layout process calculates the layoutFrame with open ended height. If set to ´NO` then the current bounds of the receiver determine the height.
*/
@property (nonatomic, assign) BOOL layoutFrameHeightIsConstrainedByBounds;
/**
@name Layouting
*/
/**
Discards the current <layoutFrame> and creates a new one based on the <attributedString>.
*/
- (void)relayoutText;
/**
The layouter to use for the receiver. Created by default.
By default this is generated automatically for the current <attributedString>. You can also supply your own if you require special layouting behavior.
*/
@property (atomic, strong) DTCoreTextLayouter *layouter;
/**
The layout frame to use for the receiver. Created by default.
A layout frame is basically one rectangle, inset by the <edgeInsets>. By default this is automatically generated for the current <attributedString>. You can also create a <DTCoreTextLayoutFrame> seperately and set this property to display the layout frame. This is usedful for example if you layout entire e-book and then set the <layoutFrame> for displaying individual pages.
*/
@property (atomic, strong) DTCoreTextLayoutFrame *layoutFrame;
/**
@name Working with Custom Subviews
*/
/**
Removes all custom subviews (excluding views representing links) from the receiver.
*/
- (void)removeAllCustomViews;
/**
Removes all custom subviews representing links from the receiver
*/
- (void)removeAllCustomViewsForLinks;
/**
Removes invisible custom subviews and lays out subviews visible in the given rectangle
@param rect The bounds of the visible area to layout custom subviews in.
*/
- (void)layoutSubviewsInRect:(CGRect)rect;
/**
@name Providing Content
*/
/**
The attributed string to display in the receiver
*/
@property (nonatomic, copy) NSAttributedString *attributedString;
/**
The delegate that is in charge of supplying custom behavior for the receiver. It must conform to <DTAttributedTextContentViewDelegate> and provide custom subviews, link buttons, etc.
*/
@property (nonatomic, DT_WEAK_PROPERTY) IBOutlet id <DTAttributedTextContentViewDelegate> delegate;
/**
@name Customizing Content Display
*/
/**
The insets to apply around the text content
*/
@property (nonatomic) UIEdgeInsets edgeInsets;
/**
Specifies if the receiver should draw image text attachments.
Set to `NO` if you use the delegate methods to provide custom subviews to display images.
*/
@property (nonatomic) BOOL shouldDrawImages;
/**
Specified if the receiver should draw hyperlinks.
If set to `NO` then your custom subview/button for hyperlinks is responsible for displaying hyperlinks. You can use <DTLinkButton> to have links show a differently for normal and highlighted style
*/
@property (nonatomic) BOOL shouldDrawLinks;
/**
Specifies if the receiver should layout custom subviews in layoutSubviews.
If set to `YES` then all custom subviews will always be layouted. Set to `NO` to only layout visible subviews, e.g. in a scroll view. Defaults to `YES` if used stand-alone, `NO` inside a <DTAttributedTextView>.
*/
@property (nonatomic) BOOL shouldLayoutCustomSubviews;
/**
The amount by which all contents of the receiver will offset of display and subview layouting
*/
@property (nonatomic) CGPoint layoutOffset;
/**
The offset to apply for drawing the background.
If you set a pattern color as background color you can have the pattern phase be offset by this value.
*/
@property (nonatomic) CGSize backgroundOffset;
/**
An integer bit mask that determines how the receiver relayouts its contents when its bounds change.
When the views bounds change, that view automatically re-layouts its text according to the relayout mask. You specify the value of this mask by combining the constants described in DTAttributedTextContentViewRelayoutMask using the C bitwise OR operator. Combining these constants lets you specify which dimensions will cause a re-layout if modified. The default value of this property is DTAttributedTextContentViewRelayoutOnWidthChanged, which indicates that the text will be re-layouted if the width changes, but not if the height changes.
*/
@property (nonatomic) DTAttributedTextContentViewRelayoutMask relayoutMask;
@end
/**
You can globally customize the layer class to be used for new instances of <DTAttributedTextContentView>. By itself it makes most sense to go with the default `CALayer`. For larger bodies of text, i.e. if there is scrolling then you should use a `CATiledLayer` subclass instead.
*/
@interface DTAttributedTextContentView (Tiling)
/**
Sets the layer class globally to use in new instances of content views. Defaults to `CALayer`.
While being fine for most use cases you should use a `CATiledLayer` subclass for anything larger than a screen full, e.g. in scroll views.
@param layerClass The class to use, should be a `CALayer` subclass
*/
+ (void)setLayerClass:(Class)layerClass;
/**
The current layer class that is used for new instances
@returns The `CALayer` subclass that new instances are using
*/
+ (Class)layerClass;
@end
/**
Methods for drawing the content view
*/
@interface DTAttributedTextContentView (Drawing)
/**
Creates an image from a part of the receiver's content view
@param bounds The bounds of the content to draw
@param options The drawing options to apply when drawing
@see [DTCoreTextLayoutFrame drawInContext:options:] for a list of available drawing options
@returns A `UIImage` with the specified content
*/
- (UIImage *)contentImageWithBounds:(CGRect)bounds options:(DTCoreTextLayoutFrameDrawingOptions)options;
@end
/**
Methods for getting cursor position and frame. Those are convenience methods that call through to the layoutFrame property which has the same coordinate system as the receiver.
*/
@interface DTAttributedTextContentView (Cursor)
/**
Determines the closest string index to a point in the receiver's frame.
This can be used to find the cursor position to position an input caret at.
@param point The point
@returns The resulting string index
*/
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point;
/**
The rectangle to draw a caret for a given index
@param index The string index for which to determine a cursor frame
@returns The cursor rectangle
*/
- (CGRect)cursorRectAtIndex:(NSInteger)index;
@end
#endif
File diff suppressed because it is too large Load Diff
@@ -1,136 +0,0 @@
//
// DTAttributedTextView.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/12/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <UIKit/UIKit.h>
#import "DTAttributedTextContentView.h"
@class DTAttributedTextView;
/**
This view is designed to be a replacement for `UITextView`. It is a `UIScrollView` subclass and creates a <DTAttributedTextContentView> as content view for displaying the text.
The content view of type <DTAttributedTextContentView> is created lazily. You should not set values on it directly if you use it in conjunction with this class for scrolling.
*/
@interface DTAttributedTextView : UIScrollView
{
// ivars needed by subclasses
DTAttributedTextContentView *_attributedTextContentView;
}
/**
@name Providing Content
*/
/**
The attributed text to be displayed in the text content view of the receiver.
*/
@property (nonatomic, strong) NSAttributedString *attributedString;
/**
A delegate implementing DTAttributedTextContentViewDelegate to provide custom subviews for images and links.
*/
@property (nonatomic, DT_WEAK_PROPERTY) IBOutlet id <DTAttributedTextContentViewDelegate> textDelegate;
/**
Performs a new layout pass on the receiver. This destroys the frame setter, calls relayoutText on the content view and marks the receiver as needing layout so that custom subviews get appropriately sized.
*/
- (void)relayoutText;
/**
@name Accessing Subviews
*/
/**
References to the DTAttributedTextContentView that display the text. This is not named contentView because this class inherits from `UIScrollView` which has an internal property of this name
*/
@property (nonatomic, strong, readonly) DTAttributedTextContentView *attributedTextContentView;
/**
A view to be displayed behind the text content view
*/
@property (nonatomic, strong) IBOutlet UIView *backgroundView;
/**
@name Customizing Display
*/
/**
If the content view of the receiver should draw links. Set to `NO` if displaying links as custom views via textDelegate;
Defaults to `YES` if you supply your own link drawing then set this property to NO and supply your custom view (e.g. <DTLinkButton>) via the <textDelegate>.
*/
@property (nonatomic, assign) BOOL shouldDrawLinks;
/**
If the content view of the receiver should draw images. Set to `NO` if displaying images as custom views via textDelegate;
Defaults to `YES` if you supply your own image drawing then set this property to NO and supply your custom image view (e.g. <DTLazyImageView>) via the <textDelegate>.
*/
@property (nonatomic, assign) BOOL shouldDrawImages;
/**
@name Customizing Content View
*/
/**
You can override this method to provide a different class to use for the content view. If you replace the content view class then it should inherit from <DTAttributedTextContentView> which is also the default.
@returns The class to use for the content view.
*/
- (Class)classForContentView;
/**
@name User Interaction
*/
/**
Scrolls the receiver to the anchor with the given name to the top.
@param anchorName The name of the href anchor.
@param animated `YES` if the movement should be animated.
*/
- (void)scrollToAnchorNamed:(NSString *)anchorName animated:(BOOL)animated;
/**
Scrolls the receiver until the text in the specified range is visible.
@param range The range of text to scroll into view.
@param animated `YES` if the movement should be animated.
*/
- (void)scrollRangeToVisible:(NSRange)range animated:(BOOL)animated;
/**
@name Working with a Cursor
*/
/**
Determines the closest string index to a point in the receiver's frame.
This can be used to find the cursor position to position an input caret at.
@param point The point
@returns The resulting string index
*/
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point;
/**
The rectangle to draw a caret for a given index
@param index The string index for which to determine a cursor frame
@returns The cursor rectangle
*/
- (CGRect)cursorRectAtIndex:(NSInteger)index;
@end
#endif
@@ -1,429 +0,0 @@
//
// DTAttributedTextView.m
// DTCoreText
//
// Created by Oliver Drobnik on 1/12/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTAttributedTextView.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <QuartzCore/QuartzCore.h>
#import "DTCoreText.h"
#import <DTFoundation/DTTiledLayerWithoutFade.h>
#import <DTFoundation/DTBlockFunctions.h>
@interface DTAttributedTextView ()
- (void)_setup;
@end
@implementation DTAttributedTextView
{
UIView *_backgroundView;
// these are pass-through, i.e. store until the content view is created
DT_WEAK_VARIABLE id textDelegate;
NSAttributedString *_attributedString;
BOOL _shouldDrawLinks;
BOOL _shouldDrawImages;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self _setup];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.attributedTextContentView.edgeInsets = self.contentInset;
// layout custom subviews for visible area
[_attributedTextContentView layoutSubviewsInRect:self.bounds];
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self _setup];
}
- (void)safeAreaInsetsDidChange
{
[super safeAreaInsetsDidChange];
}
// default
- (void)_setup
{
if (self.backgroundColor)
{
CGFloat alpha = [self.backgroundColor alphaComponent];
if (alpha < 1.0)
{
self.opaque = NO;
}
else
{
self.opaque = YES;
}
}
else
{
self.backgroundColor = [DTColor whiteColor];
self.opaque = YES;
}
self.autoresizesSubviews = NO;
self.clipsToBounds = YES;
// defaults
_shouldDrawLinks = YES;
_shouldDrawImages = YES;
}
// override class e.g. for mutable content view
- (Class)classForContentView
{
return [DTAttributedTextContentView class];
}
#pragma mark External Methods
- (void)scrollToAnchorNamed:(NSString *)anchorName animated:(BOOL)animated
{
NSRange range = [self.attributedTextContentView.attributedString rangeOfAnchorNamed:anchorName];
if (range.location != NSNotFound)
{
[self scrollRangeToVisible:range animated:animated];
}
}
- (void)scrollRangeToVisible:(NSRange)range animated:(BOOL)animated
{
// get the line of the first index of the anchor range
DTCoreTextLayoutLine *line = [self.attributedTextContentView.layoutFrame lineContainingIndex:range.location];
// make sure we don't scroll too far
CGFloat maxScrollPos = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom + self.contentInset.top;
CGFloat scrollPos = MIN(line.frame.origin.y, maxScrollPos);
// scroll
[self setContentOffset:CGPointMake(0, scrollPos) animated:animated];
}
- (void)relayoutText
{
DT_WEAK_VARIABLE typeof(self) weakSelf = self;
DTBlockPerformSyncIfOnMainThreadElseAsync(^{
DTAttributedTextView *strongSelf = weakSelf;
// need to reset the layouter because otherwise we get the old framesetter or cached layout frames
strongSelf->_attributedTextContentView.layouter = nil;
// here we're layouting the entire string, might be more efficient to only relayout the paragraphs that contain these attachments
[strongSelf->_attributedTextContentView relayoutText];
// layout custom subviews for visible area
[strongSelf setNeedsLayout];
});
}
#pragma mark - Working with a Cursor
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point
{
// the point is in the coordinate system of the receiver, need to convert into those of the content view first
CGPoint pointInContentView = [self.attributedTextContentView convertPoint:point fromView:self];
return [self.attributedTextContentView closestCursorIndexToPoint:pointInContentView];
}
- (CGRect)cursorRectAtIndex:(NSInteger)index
{
CGRect rectInContentView = [self.attributedTextContentView cursorRectAtIndex:index];
// the point is in the coordinate system of the content view, need to convert into those of the receiver first
CGRect rect = [self.attributedTextContentView convertRect:rectInContentView toView:self];
return rect;
}
#pragma mark Notifications
- (void)contentViewDidLayout:(NSNotification *)notification
{
DT_WEAK_VARIABLE typeof(self) weakSelf = self;
DTBlockPerformSyncIfOnMainThreadElseAsync(^{
DTAttributedTextView *strongSelf = weakSelf;
NSDictionary *userInfo = [notification userInfo];
CGRect optimalFrame = [[userInfo objectForKey:@"OptimalFrame"] CGRectValue];
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
// ignore possibly delayed layout notification for a different width
if (optimalFrame.size.width == frame.size.width)
{
strongSelf->_attributedTextContentView.frame = optimalFrame;
strongSelf.contentSize = [strongSelf->_attributedTextContentView intrinsicContentSize];
}
});
}
#pragma mark Properties
- (DTAttributedTextContentView *)attributedTextContentView
{
if (!_attributedTextContentView)
{
// subclasses can specify a DTAttributedTextContentView subclass instead
Class classToUse = [self classForContentView];
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
if (frame.size.width<=0 || frame.size.height<=0)
{
frame = CGRectZero;
}
// make sure we always have a tiled layer
Class previousLayerClass = nil;
// for DTAttributedTextContentView subclasses we force a tiled layer
if ([classToUse isSubclassOfClass:[DTAttributedTextContentView class]])
{
Class layerClass = [DTAttributedTextContentView layerClass];
if (![layerClass isSubclassOfClass:[CATiledLayer class]])
{
[DTAttributedTextContentView setLayerClass:[DTTiledLayerWithoutFade class]];
previousLayerClass = layerClass;
}
}
_attributedTextContentView = [[classToUse alloc] initWithFrame:frame];
// restore previous layer class if we changed the layer class for the content view
if (previousLayerClass)
{
[DTAttributedTextContentView setLayerClass:previousLayerClass];
}
_attributedTextContentView.userInteractionEnabled = YES;
_attributedTextContentView.backgroundColor = self.backgroundColor;
_attributedTextContentView.shouldLayoutCustomSubviews = NO; // we call layout when scrolling
// adjust opaqueness based on background color alpha
CGFloat alpha = [self.backgroundColor alphaComponent];
if (alpha < 1.0)
{
_attributedTextContentView.opaque = NO;
}
else
{
_attributedTextContentView.opaque = YES;
}
// set text delegate if it was set before instantiation of content view
_attributedTextContentView.delegate = self->_textDelegate;
// pass on setting
_attributedTextContentView.shouldDrawLinks = _shouldDrawLinks;
// notification that tells us about the actual size of the content view
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentViewDidLayout:) name:DTAttributedTextContentViewDidFinishLayoutNotification object:_attributedTextContentView];
// temporary frame to specify the width
_attributedTextContentView.frame = frame;
// set text we previously got, this also triggers a relayout
_attributedTextContentView.attributedString = _attributedString;
// this causes a relayout and the resulting notification will allow us to set the final frame
[self addSubview:_attributedTextContentView];
}
return _attributedTextContentView;
}
- (void)setBackgroundColor:(DTColor *)newColor
{
if ([newColor alphaComponent] < 1.0)
{
super.backgroundColor = newColor;
_attributedTextContentView.backgroundColor = [DTColor clearColor];
self.opaque = NO;
}
else
{
super.backgroundColor = newColor;
if (_attributedTextContentView.opaque)
{
_attributedTextContentView.backgroundColor = newColor;
}
}
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
[super setContentInset:contentInset];
// height does not matter, that will be determined anyhow
CGRect contentFrame = CGRectMake(0, 0, self.frame.size.width - self.contentInset.left - self.contentInset.right, _attributedTextContentView.frame.size.height);
if (CGRectEqualToRect(contentFrame, self.attributedTextContentView.frame))
{
self.attributedTextContentView.frame = contentFrame;
}
}
- (UIView *)backgroundView
{
if (!_backgroundView)
{
_backgroundView = [[UIView alloc] initWithFrame:self.bounds];
_backgroundView.backgroundColor = [DTColor whiteColor];
// default is no interaction because background should have no interaction
_backgroundView.userInteractionEnabled = NO;
[self insertSubview:_backgroundView belowSubview:self.attributedTextContentView];
// make content transparent so that we see the background
_attributedTextContentView.backgroundColor = [DTColor clearColor];
_attributedTextContentView.opaque = NO;
}
return _backgroundView;
}
- (void)setBackgroundView:(UIView *)backgroundView
{
if (_backgroundView != backgroundView)
{
[_backgroundView removeFromSuperview];
_backgroundView = backgroundView;
if (_attributedTextContentView)
{
[self insertSubview:_backgroundView belowSubview:_attributedTextContentView];
}
else
{
[self addSubview:_backgroundView];
}
if (_backgroundView)
{
// make content transparent so that we see the background
_attributedTextContentView.backgroundColor = [DTColor clearColor];
_attributedTextContentView.opaque = NO;
}
else
{
_attributedTextContentView.backgroundColor = [DTColor whiteColor];
_attributedTextContentView.opaque = YES;
}
}
}
- (void)setAttributedString:(NSAttributedString *)string
{
_attributedString = string;
// might need layout for visible custom views
[self setNeedsLayout];
if (_attributedTextContentView)
{
// pass it along if contentView already exists
_attributedTextContentView.attributedString = string;
// this causes a relayout and the resulting notification will allow us to set the frame and contentSize
}
}
- (NSAttributedString *)attributedString
{
return _attributedString;
}
- (void)setFrame:(CGRect)frame
{
CGRect oldFrame = self.frame;
if (!CGRectEqualToRect(oldFrame, frame))
{
[super setFrame:frame]; // need to set own frame first because layout completion needs this updated frame
if (oldFrame.size.width != frame.size.width)
{
// height does not matter, that will be determined anyhow
CGRect contentFrame = CGRectMake(0, 0, frame.size.width - self.contentInset.left - self.contentInset.right, _attributedTextContentView.frame.size.height);
_attributedTextContentView.frame = contentFrame;
}
}
}
- (void)setTextDelegate:(id<DTAttributedTextContentViewDelegate>)aTextDelegate
{
// store unsafe pointer to delegate because we might not have a contentView yet
self->_textDelegate = aTextDelegate;
// set it if possible, otherwise it will be set in contentView lazy property
_attributedTextContentView.delegate = aTextDelegate;
}
- (id<DTAttributedTextContentViewDelegate>)textDelegate
{
return _attributedTextContentView.delegate ?: self->_textDelegate;
}
- (void)setShouldDrawLinks:(BOOL)shouldDrawLinks
{
_shouldDrawLinks = shouldDrawLinks;
_attributedTextContentView.shouldDrawLinks = _shouldDrawLinks;
}
- (void)setShouldDrawImages:(BOOL)shouldDrawImages
{
_shouldDrawImages = shouldDrawImages;
_attributedTextContentView.shouldDrawImages = _shouldDrawImages;
}
@synthesize attributedTextContentView = _attributedTextContentView;
@synthesize attributedString = _attributedString;
@synthesize textDelegate = _textDelegate;
@synthesize shouldDrawLinks = _shouldDrawLinks;
@end
#endif
@@ -1,17 +0,0 @@
//
// DTHTMLElementBR.h
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHTMLElement.h"
/**
Specialized subclass of <DTHTMLElement> that represents a line break.
*/
@interface DTBreakHTMLElement : DTHTMLElement
@end
@@ -1,22 +0,0 @@
//
// DTHTMLElementBR.m
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTBreakHTMLElement.h"
@implementation DTBreakHTMLElement
- (NSAttributedString *)attributedString
{
@synchronized(self)
{
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
return [[NSAttributedString alloc] initWithString:UNICODE_LINE_FEED attributes:attributes];
}
}
@end
-264
View File
@@ -1,264 +0,0 @@
//
// DTCSSListStyle.h
// DTCoreText
//
// Created by Oliver Drobnik on 8/11/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
/**
List Styles
*/
typedef NS_ENUM(NSUInteger, DTCSSListStyleType)
{
/**
The list style should be inherited from the parent
*/
DTCSSListStyleTypeInherit = 0,
/**
No list style
*/
DTCSSListStyleTypeNone,
/**
Circle bullet list style
*/
DTCSSListStyleTypeCircle,
/**
Decimal number list style
*/
DTCSSListStyleTypeDecimal,
/**
Decimal number list style with a leading zero
*/
DTCSSListStyleTypeDecimalLeadingZero,
/**
Disc bullet list style
*/
DTCSSListStyleTypeDisc,
/**
Square bullet list style
*/
DTCSSListStyleTypeSquare,
/**
Numbered list style with uppercase letters
*/
DTCSSListStyleTypeUpperAlpha,
/**
Numbered list style with uppercase letters
*/
DTCSSListStyleTypeUpperLatin,
/**
Numbered list style with uppercase roman numbers
*/
DTCSSListStyleTypeUpperRoman,
/**
Numbered list style with lowercase letters
*/
DTCSSListStyleTypeLowerAlpha,
/**
Numbered list style with lowercase letters
*/
DTCSSListStyleTypeLowerLatin,
/**
Numbered list style with lowercase roman numbers
*/
DTCSSListStyleTypeLowerRoman,
/**
Plus bullet list style
*/
DTCSSListStyleTypePlus,
/**
Underscore bullet list style
*/
DTCSSListStyleTypeUnderscore,
/**
Image bullet list style
*/
DTCSSListStyleTypeImage,
/**
Value used to represent an invalid list style
*/
DTCSSListStyleTypeInvalid = NSIntegerMax
};
/**
List Marker Positions
*/
typedef NS_ENUM(NSUInteger, DTCSSListStylePosition)
{
/**
List position should be inherited
*/
DTCSSListStylePositionInherit = 0,
/**
List prefix position inside
*/
DTCSSListStylePositionInside,
/**
List prefix position outside
*/
DTCSSListStylePositionOutside,
/**
Value used to represent an invalid list style position
*/
DTCSSListStylePositionInvalid = NSIntegerMax
};
/**
This class is the equivalent of `NSTextList` on Mac with the added handling of the marker position.
*/
@interface DTCSSListStyle : NSObject <NSCoding>
/**
@name Getting Types from Strings
*/
/**
Convert a string into a list style type.
@param string The string to convert
*/
+ (DTCSSListStyleType)listStyleTypeFromString:(NSString *)string;
/**
Convert a string into a marker position.
@param string The string to convert
*/
+ (DTCSSListStylePosition)listStylePositionFromString:(NSString *)string;
/**
@name Creating List Styles
*/
/**
Creates a list style from the passed CSS style dictionary
@param styles A CSS style dictionary from which the construct a suitable list style
*/
- (id)initWithStyles:(NSDictionary *)styles;
/**
@name Working with CSS Styles
*/
/**
Update the receiver from the CSS styles dictionary passed
@param styles A dictionary of CSS styles.
*/
- (void)updateFromStyleDictionary:(NSDictionary *)styles;
/**
@name Working with Prefixes
*/
/**
Returns the prefix for lists of the receiver's settings.
@param counter The counter value to use for ordered lists.
@returns The prefix string to prepend to list items.
*/
- (NSString *)prefixWithCounter:(NSInteger)counter;
/**
@name Managing Item Numbering
*/
/**
Sets the starting item number for the text list.
The default value is `1`. This value will be used only for ordered lists, and ignored in other cases.
@param itemNum The item number.
*/
- (void)setStartingItemNumber:(NSInteger)itemNum;
/**
Returns the starting item number for the text list.
The default value is `1`. This value will be used only for ordered lists, and ignored in other cases.
@returns The item number.
*/
- (NSInteger)startingItemNumber;
/**
@name Comparing Lists
*/
/**
Determine if another list style has equivalent settings. Note that this does not mean that they are identical, only that they look the same.
@param otherListStyle The other list style to compare the receiver with
@returns `YES` if the other list style has the same values
*/
- (BOOL)isEqualToListStyle:(DTCSSListStyle *)otherListStyle;
/**
@name Getting Information about Lists
*/
/**
Returns if the receiver is an ordered or unordered list
@returns `YES` if the receiver is ordered, `NO` if it is unordered
*/
- (BOOL)isOrdered;
/**
If the list style is inherited.
@warn This is not implemented.
*/
@property (nonatomic, assign) BOOL inherit;
/**
The type of the text list. See DTCSSListStyleType for available types
*/
@property (nonatomic, assign) DTCSSListStyleType type;
/**
The position of the marker in the prefix. See DTCSSListStylePosition for available positions.
*/
@property (nonatomic, assign) DTCSSListStylePosition position;
/**
The image name to use for the marker
*/
@property (nonatomic, copy) NSString *imageName;
@end
-483
View File
@@ -1,483 +0,0 @@
//
// DTCSSListStyle.m
// DTCoreText
//
// Created by Oliver Drobnik on 8/11/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCSSListStyle.h"
#import "DTCoreTextConstants.h"
#import "NSScanner+HTML.h"
#import "NSNumber+RomanNumerals.h"
@interface DTCSSListStyle ()
- (void)updateFromStyleDictionary:(NSDictionary *)styles;
@property (nonatomic, assign) NSInteger startingItemNumber;
@end
@implementation DTCSSListStyle
{
BOOL _inherit;
DTCSSListStyleType _type;
DTCSSListStylePosition _position;
NSString *_imageName;
NSInteger _startingItemNumber;
}
- (id)initWithStyles:(NSDictionary *)styles
{
self = [super init];
if (self)
{
// default
_position = DTCSSListStylePositionOutside;
_startingItemNumber = 1;
[self updateFromStyleDictionary:styles];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
_inherit = [aDecoder decodeBoolForKey:@"inherit"];
_type = [aDecoder decodeIntegerForKey:@"type"];
_position = [aDecoder decodeIntegerForKey:@"position"];
_imageName = [aDecoder decodeObjectForKey:@"imageName"];
_startingItemNumber = [aDecoder decodeIntegerForKey:@"startingItemNumber"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeBool:_inherit forKey:@"inherit"];
[aCoder encodeInteger:_type forKey:@"type"];
[aCoder encodeInteger:_position forKey:@"position"];
[aCoder encodeObject:_imageName forKey:@"imageName"];
[aCoder encodeInteger:_startingItemNumber forKey:@"startingItemNumber"];
}
// convert string to listStyleType
+ (DTCSSListStyleType)listStyleTypeFromString:(NSString *)string
{
if (!string)
{
return DTCSSListStyleTypeInvalid;
}
// always compare lower case
string = [string lowercaseString];
if ([string isEqualToString:@"inherit"])
{
return DTCSSListStyleTypeInherit;
}
else if ([string isEqualToString:@"none"])
{
return DTCSSListStyleTypeNone;
}
else if ([string isEqualToString:@"circle"])
{
return DTCSSListStyleTypeCircle;
}
else if ([string isEqualToString:@"square"])
{
return DTCSSListStyleTypeSquare;
}
else if ([string isEqualToString:@"decimal"])
{
return DTCSSListStyleTypeDecimal;
}
else if ([string isEqualToString:@"decimal-leading-zero"])
{
return DTCSSListStyleTypeDecimalLeadingZero;
}
else if ([string isEqualToString:@"disc"])
{
return DTCSSListStyleTypeDisc;
}
else if ([string isEqualToString:@"upper-alpha"]||[string isEqualToString:@"upper-latin"])
{
return DTCSSListStyleTypeUpperAlpha;
}
else if ([string isEqualToString:@"lower-alpha"]||[string isEqualToString:@"lower-latin"])
{
return DTCSSListStyleTypeLowerAlpha;
}
else if ([string isEqualToString:@"lower-roman"])
{
return DTCSSListStyleTypeLowerRoman;
}
else if ([string isEqualToString:@"upper-roman"])
{
return DTCSSListStyleTypeUpperRoman;
}
else if ([string isEqualToString:@"plus"])
{
return DTCSSListStyleTypePlus;
}
else if ([string isEqualToString:@"underscore"])
{
return DTCSSListStyleTypeUnderscore;
}
else
{
return DTCSSListStyleTypeNone;
}
}
+ (DTCSSListStylePosition)listStylePositionFromString:(NSString *)string
{
if (!string)
{
return DTCSSListStylePositionInvalid;
}
// always compare lower case
string = [string lowercaseString];
if ([string isEqualToString:@"inherit"])
{
return DTCSSListStylePositionInherit;
}
else if ([string isEqualToString:@"inside"])
{
return DTCSSListStylePositionInside;
}
else if ([string isEqualToString:@"outside"])
{
return DTCSSListStylePositionOutside;
}
else
{
return DTCSSListStylePositionInherit;
}
}
// returns NO if not a valid type
- (BOOL)setTypeWithString:(NSString *)string
{
DTCSSListStyleType type = [DTCSSListStyle listStyleTypeFromString:string];
if (type == DTCSSListStyleTypeInvalid)
{
return NO;
}
_type = type;
return YES;
}
// returns NO if not a valid type
- (BOOL)setPositionWithString:(NSString *)string
{
DTCSSListStylePosition position = [DTCSSListStyle listStylePositionFromString:string];
if (position == DTCSSListStylePositionInvalid)
{
return NO;
}
_position = position;
return YES;
}
- (void)updateFromStyleDictionary:(NSDictionary *)styles
{
NSString *shortHand = [[styles objectForKey:@"list-style"] lowercaseString];
if (shortHand)
{
if ([shortHand isEqualToString:@"inherit"])
{
_inherit = YES;
return;
}
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
BOOL typeWasSet = NO;
BOOL positionWasSet = NO;
for (NSString *oneComponent in components)
{
if ([oneComponent hasPrefix:@"url"])
{
// list-style-image
NSString *urlString;
NSScanner *scanner = [NSScanner scannerWithString:oneComponent];
if ([scanner scanCSSURL:&urlString])
{
self.imageName = urlString;
continue;
}
}
if (!typeWasSet && [self setTypeWithString:oneComponent])
{
typeWasSet = YES;
continue;
}
if (!positionWasSet && [self setPositionWithString:oneComponent])
{
positionWasSet = YES;
continue;
}
}
return;
}
// not a short hand, set from individual types
[self setTypeWithString:[styles objectForKey:@"list-style-type"]];
[self setPositionWithString:[styles objectForKey:@"list-style-position"]];
NSObject *tmpValue = [styles objectForKey:@"list-style-image"];
if ([tmpValue isKindOfClass:NSString.class])
{
// extract just the name
NSString *urlString;
NSScanner *scanner = [NSScanner scannerWithString:(NSString *)tmpValue];
if ([scanner scanCSSURL:&urlString])
{
self.imageName = urlString;
}
}
}
#ifndef COVERAGE
// exclude methods from coverage testing
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ %p type=%d position=%d>", NSStringFromClass([self class]), self, (int)_type, (int)_position];
}
- (NSUInteger)hash
{
NSUInteger calcHash = 7;
calcHash = calcHash*31 + [_imageName hash];
calcHash = calcHash*31 + (NSUInteger)_type;
calcHash = calcHash*31 + (NSUInteger)_position;
calcHash = calcHash*31 + (NSUInteger)_startingItemNumber;
calcHash = calcHash*31 + (NSUInteger)_inherit;
return calcHash;
}
#endif
/*
Note: this is not isEqual: because on iOS 7 -[NSMutableAttributedString initWithString:attributes:] calls this via -[NSArray isEqualToArray:]. There isEqual: needs to be returning NO, because otherwise there is some weird internal caching side effect where it reuses previous list arrays
*/
- (BOOL)isEqualToListStyle:(DTCSSListStyle *)otherListStyle
{
if (!otherListStyle)
{
return NO;
}
if (otherListStyle == self)
{
return YES;
}
if (![otherListStyle isKindOfClass:[DTCSSListStyle class]])
{
return NO;
}
if (_inherit != otherListStyle->_inherit)
{
return NO;
}
if (_type != otherListStyle->_type)
{
return NO;
}
if (_position != otherListStyle->_position)
{
return NO;
}
if (_startingItemNumber != otherListStyle->_startingItemNumber)
{
return NO;
}
if (_imageName == otherListStyle->_imageName)
{
return YES;
}
return ([_imageName isEqualToString:otherListStyle->_imageName]);
}
#pragma mark Copying
- (id)copyWithZone:(NSZone *)zone
{
DTCSSListStyle *newStyle = [[DTCSSListStyle allocWithZone:zone] init];
newStyle.type = self.type;
newStyle.position = self.position;
newStyle.imageName = self.imageName;
newStyle.startingItemNumber = self.startingItemNumber;
return newStyle;
}
#pragma mark Utilities
- (NSString *)prefixWithCounter:(NSInteger)counter
{
NSString *token = nil;
DTCSSListStyleType listStyleType = _type;
if (self.imageName)
{
listStyleType = DTCSSListStyleTypeImage;
}
switch (listStyleType)
{
case DTCSSListStyleTypeNone:
case DTCSSListStyleTypeInherit: // should never be called with inherit
case DTCSSListStyleTypeInvalid:
{
return nil;
}
case DTCSSListStyleTypeImage:
{
token = UNICODE_OBJECT_PLACEHOLDER;
break;
}
case DTCSSListStyleTypeCircle:
{
token = @"\u25e6";
break;
}
case DTCSSListStyleTypeSquare:
{
token = @"\u25aa";
break;
}
case DTCSSListStyleTypeDecimal:
{
token = [NSString stringWithFormat:@"%d.", (int)counter];
break;
}
case DTCSSListStyleTypeDecimalLeadingZero:
{
token = [NSString stringWithFormat:@"%02d.", (int)counter];
break;
}
case DTCSSListStyleTypeDisc:
{
token = @"\u2022";
break;
}
case DTCSSListStyleTypeUpperAlpha:
case DTCSSListStyleTypeUpperLatin:
{
char letter = 'A' + (char)(counter - 1);
token = [NSString stringWithFormat:@"%c.", letter];
break;
}
case DTCSSListStyleTypeLowerAlpha:
case DTCSSListStyleTypeLowerLatin:
{
char letter = 'a' + (char)(counter - 1);
token = [NSString stringWithFormat:@"%c.", letter];
break;
}
case DTCSSListStyleTypePlus:
{
token = @"+";
break;
}
case DTCSSListStyleTypeUnderscore:
{
token = @"_";
break;
}
case DTCSSListStyleTypeUpperRoman:
{
token = [NSString stringWithFormat:@"%@.",[@(counter) romanNumeral]];
break;
}
case DTCSSListStyleTypeLowerRoman:
{
token = [NSString stringWithFormat:@"%@.",[[@(counter) romanNumeral] lowercaseString]];
break;
}
}
if (_position == DTCSSListStylePositionInside)
{
// iOS needs second tab, Mac ignores position outside
#if TARGET_OS_IPHONE
return [NSString stringWithFormat:@"\x09\x09%@", token];
#else
return [NSString stringWithFormat:@"\x09%@\x09", token];
#endif
}
else
{
return [NSString stringWithFormat:@"\x09%@\x09", token];
}
}
- (BOOL)isOrdered
{
switch (_type)
{
case DTCSSListStyleTypeDecimal:
case DTCSSListStyleTypeDecimalLeadingZero:
case DTCSSListStyleTypeUpperAlpha:
case DTCSSListStyleTypeUpperLatin:
case DTCSSListStyleTypeLowerAlpha:
case DTCSSListStyleTypeLowerLatin:
return YES;
default:
return NO;
}
}
#pragma mark Properties
@synthesize inherit = _inherit;
@synthesize type = _type;
@synthesize position = _position;
@synthesize imageName = _imageName;
@synthesize startingItemNumber = _startingItemNumber;
@end
// TO DO: Implement image
@@ -1,86 +0,0 @@
//
// DTCSSStylesheet.h
// DTCoreText
//
// Created by Oliver Drobnik on 9/5/11.
// Copyright (c) 2011 Drobnik.com. All rights reserved.
//
#import <Foundation/Foundation.h>
@class DTHTMLElement;
/**
This class represents a CSS style sheet used for specifying formatting for certain CSS selectors.
It supports matching styles by class, by id or by tag name. Hierarchy matching is not supported yet.
*/
@interface DTCSSStylesheet : NSObject <NSCopying>
/**
@name Creating Stylesheets
*/
/**
Creates the default stylesheet.
This stylesheet is based on the standard styles that Webkit provides for these tags. This stylesheet is loaded from default.css.
*/
+ (DTCSSStylesheet *)defaultStyleSheet;
/**
Creates a stylesheet with a given style block
@param css The CSS string for the style block
*/
- (id)initWithStyleBlock:(NSString *)css;
/**
@name Working with CSS Style Blocks
*/
/**
Parses a style block string and adds the found style rules to the receiver.
@param css The CSS string for the style block
*/
- (void)parseStyleBlock:(NSString *)css;
/**
Merges styles from given stylesheet into the receiver
@param stylesheet the stylesheet to merge
*/
- (void)mergeStylesheet:(DTCSSStylesheet *)stylesheet;
/**
@name Accessing Style Information
*/
/**
Returns a dictionary that contains the merged style for a given element and the applicable style rules from the receiver.
@param element The HTML element.
@param matchedSelectors The CSS selectors that caused a match
@param ignoreInlineStyle If `YES` then the inline styles of the element will be ignored and only the receiver's styles used
@returns The merged style dictionary containing only styles which selector matches the element
*/
- (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element matchedSelectors:(NSSet * __autoreleasing*)matchedSelectors ignoreInlineStyle:(BOOL)ignoreInlineStyle;
/**
Returns a dictionary of the styles of the receiver
*/
- (NSDictionary *)styles;
/**
Returns an ordered (by declaration) set of the selectors for all of the styles.
*/
- (NSArray *)orderedSelectors;
@end
-976
View File
@@ -1,976 +0,0 @@
//
// DTCSSStylesheet.m
// DTCoreText
//
// Created by Oliver Drobnik on 9/5/11.
// Copyright (c) 2011 Drobnik.com. All rights reserved.
//
#import "DTCSSStylesheet.h"
#import "DTCSSListStyle.h"
#import "DTHTMLElement.h"
#import "NSScanner+HTML.h"
#import "NSString+CSS.h"
#import "NSString+HTML.h"
@implementation DTCSSStylesheet
{
NSMutableDictionary *_styles;
NSMutableDictionary *_orderedSelectorWeights;
NSMutableArray *_orderedSelectors;
}
#pragma mark Creating Stylesheets
+ (DTCSSStylesheet *)defaultStyleSheet
{
static DTCSSStylesheet *defaultDTCSSStylesheet = nil;
if (defaultDTCSSStylesheet)
{
return defaultDTCSSStylesheet;
}
@synchronized(self)
{
if (!defaultDTCSSStylesheet)
{
#if SWIFT_PACKAGE
// get resource bundle via macro
NSString *path = [SWIFTPM_MODULE_BUNDLE pathForResource:@"default" ofType:@"css"];
#else
NSBundle *bundle = [NSBundle bundleForClass:self];
NSString *path = [[NSBundle bundleForClass:self] pathForResource:@"default" ofType:@"css"];
// Cocoapods uses a separate Resources bundle to include default.css
if (!path)
{
NSString *resourcesBundlePath = [bundle pathForResource:@"Resources" ofType:@"bundle"];
NSBundle *resourcesBundle = [NSBundle bundleWithPath:resourcesBundlePath];
path = [resourcesBundle pathForResource:@"default" ofType:@"css"];
}
#endif
NSAssert(path != nil, @"Missing default.css");
NSString *cssString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
defaultDTCSSStylesheet = [[DTCSSStylesheet alloc] initWithStyleBlock:cssString];
}
}
return defaultDTCSSStylesheet;
}
- (id)initWithStyleBlock:(NSString *)css
{
self = [super init];
if (self)
{
_styles = [[NSMutableDictionary alloc] init];
_orderedSelectorWeights = [[NSMutableDictionary alloc] init];
_orderedSelectors = [[NSMutableArray alloc] init];
[self parseStyleBlock:css];
}
return self;
}
- (id)initWithStylesheet:(DTCSSStylesheet *)stylesheet
{
self = [super init];
if (self)
{
_styles = [[NSMutableDictionary alloc] init];
_orderedSelectorWeights = [[NSMutableDictionary alloc] init];
_orderedSelectors = [[NSMutableArray alloc] init];
[self mergeStylesheet:stylesheet];
}
return self;
}
#ifndef COVERAGE
- (NSString *)description
{
return [_styles description];
}
#endif
#pragma mark Working with Style Blocks
- (void)_uncompressShorthands:(NSMutableDictionary *)styles
{
// list-style shorthand
NSString *shortHand = [[styles objectForKey:@"list-style"] lowercaseString];
if (shortHand && [shortHand isKindOfClass:[NSString class]])
{
[styles removeObjectForKey:@"list-style"];
if ([shortHand isEqualToString:@"inherit"])
{
[styles setObject:@"inherit" forKey:@"list-style-type"];
[styles setObject:@"inherit" forKey:@"list-style-position"];
return;
}
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
BOOL typeWasSet = NO;
BOOL positionWasSet = NO;
DTCSSListStyleType listStyleType = DTCSSListStyleTypeNone;
DTCSSListStylePosition listStylePosition = DTCSSListStylePositionInherit;
for (NSString *oneComponent in components)
{
if ([oneComponent hasPrefix:@"url"])
{
// list-style-image
NSScanner *scanner = [NSScanner scannerWithString:oneComponent];
if ([scanner scanCSSURL:NULL])
{
[styles setObject:oneComponent forKey:@"list-style-image"];
continue;
}
}
if (!typeWasSet)
{
// check if valid type
listStyleType = [DTCSSListStyle listStyleTypeFromString:oneComponent];
if (listStyleType != DTCSSListStyleTypeInvalid)
{
[styles setObject:oneComponent forKey:@"list-style-type"];
typeWasSet = YES;
continue;
}
}
if (!positionWasSet)
{
// check if valid position
listStylePosition = [DTCSSListStyle listStylePositionFromString:oneComponent];
if (listStylePosition != DTCSSListStylePositionInvalid)
{
[styles setObject:oneComponent forKey:@"list-style-position"];
positionWasSet = YES;
continue;
}
}
}
}
// font shorthand, see http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
shortHand = [styles objectForKey:@"font"];
if (shortHand && [shortHand isKindOfClass:[NSString class]])
{
NSString *fontStyle = @"normal";
NSArray *validFontStyles = [NSArray arrayWithObjects:@"italic", @"oblique", nil];
NSString *fontVariant = @"normal";
NSArray *validFontVariants = [NSArray arrayWithObjects:@"small-caps", nil];
BOOL fontVariantSet = NO;
NSString *fontWeight = @"normal";
NSArray *validFontWeights = [NSArray arrayWithObjects:@"bold", @"bolder", @"lighter", @"100", @"200", @"300", @"400", @"500", @"600", @"700", @"800", @"900", nil];
BOOL fontWeightSet = NO;
NSString *fontSize = @"normal";
NSArray *validFontSizes = [NSArray arrayWithObjects:@"xx-small", @"x-small", @"small", @"medium", @"large", @"x-large", @"xx-large", @"larger", @"smaller", nil];
BOOL fontSizeSet = NO;
NSArray *suffixesToIgnore = [NSArray arrayWithObjects:@"caption", @"icon", @"menu", @"message-box", @"small-caption", @"status-bar", @"inherit", nil];
NSString *lineHeight = @"normal";
NSMutableString *fontFamily = [NSMutableString string];
NSArray *components = [shortHand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
for (NSString *oneComponent in components)
{
// try font size keywords
if ([validFontSizes containsObject:oneComponent])
{
fontSize = oneComponent;
fontSizeSet = YES;
continue;
}
NSInteger slashIndex = [oneComponent rangeOfString:@"/"].location;
if (slashIndex != NSNotFound)
{
// font-size / line-height
fontSize = [oneComponent substringToIndex:slashIndex];
fontSizeSet = YES;
lineHeight = [oneComponent substringFromIndex:slashIndex+1];
continue;
}
else
{
// length
if ([oneComponent hasSuffix:@"%"] || [oneComponent hasSuffix:@"em"] || [oneComponent hasSuffix:@"px"] || [oneComponent hasSuffix:@"pt"])
{
fontSize = oneComponent;
fontSizeSet = YES;
continue;
}
}
if (fontSizeSet)
{
if ([suffixesToIgnore containsObject:oneComponent])
{
break;
}
// assume that this is part of font family
if ([fontFamily length])
{
[fontFamily appendString:@" "];
}
[fontFamily appendString:oneComponent];
}
else
{
if (!fontWeightSet && [validFontStyles containsObject:oneComponent])
{
fontStyle = oneComponent;
}
else if (!fontVariantSet && [validFontVariants containsObject:oneComponent])
{
fontVariant = oneComponent;
fontVariantSet = YES;
}
else if (!fontWeightSet && [validFontWeights containsObject:oneComponent])
{
fontWeight = oneComponent;
fontWeightSet = YES;
}
}
}
[styles removeObjectForKey:@"font"];
// size and family are mandatory, without them this is invalid
if ([fontSize length] && [fontFamily length])
{
[styles setObject:fontStyle forKey:@"font-style"];
[styles setObject:fontWeight forKey:@"font-weight"];
[styles setObject:fontVariant forKey:@"font-variant"];
[styles setObject:fontSize forKey:@"font-size"];
[styles setObject:lineHeight forKey:@"line-height"];
[styles setObject:fontFamily forKey:@"font-family"];
}
}
shortHand = [styles objectForKey:@"margin"];
if (shortHand && [shortHand isKindOfClass:[NSString class]])
{
NSArray *parts = [shortHand componentsSeparatedByString:@" "];
NSString *topMargin;
NSString *rightMargin;
NSString *bottomMargin;
NSString *leftMargin;
if ([parts count] == 4)
{
topMargin = [parts objectAtIndex:0];
rightMargin = [parts objectAtIndex:1];
bottomMargin = [parts objectAtIndex:2];
leftMargin = [parts objectAtIndex:3];
}
else if ([parts count] == 3)
{
topMargin = [parts objectAtIndex:0];
rightMargin = [parts objectAtIndex:1];
bottomMargin = [parts objectAtIndex:2];
leftMargin = [parts objectAtIndex:1];
}
else if ([parts count] == 2)
{
topMargin = [parts objectAtIndex:0];
rightMargin = [parts objectAtIndex:1];
bottomMargin = [parts objectAtIndex:0];
leftMargin = [parts objectAtIndex:1];
}
else
{
NSString *onlyValue = [parts objectAtIndex:0];
topMargin = onlyValue;
rightMargin = onlyValue;
bottomMargin = onlyValue;
leftMargin = onlyValue;
}
// only apply the ones where there is no previous direct setting
if (![styles objectForKey:@"margin-top"])
{
[styles setObject:topMargin forKey:@"margin-top"];
}
if (![styles objectForKey:@"margin-right"])
{
[styles setObject:rightMargin forKey:@"margin-right"];
}
if (![styles objectForKey:@"margin-bottom"])
{
[styles setObject:bottomMargin forKey:@"margin-bottom"];
}
if (![styles objectForKey:@"margin-left"])
{
[styles setObject:leftMargin forKey:@"margin-left"];
}
// remove the shorthand
[styles removeObjectForKey:@"margin"];
}
shortHand = [styles objectForKey:@"padding"];
if (shortHand && [shortHand isKindOfClass:[NSString class]])
{
NSArray *parts = [shortHand componentsSeparatedByString:@" "];
NSString *topPadding;
NSString *rightPadding;
NSString *bottomPadding;
NSString *leftPadding;
if ([parts count] == 4)
{
topPadding = [parts objectAtIndex:0];
rightPadding = [parts objectAtIndex:1];
bottomPadding = [parts objectAtIndex:2];
leftPadding = [parts objectAtIndex:3];
}
else if ([parts count] == 3)
{
topPadding = [parts objectAtIndex:0];
rightPadding = [parts objectAtIndex:1];
bottomPadding = [parts objectAtIndex:2];
leftPadding = [parts objectAtIndex:1];
}
else if ([parts count] == 2)
{
topPadding = [parts objectAtIndex:0];
rightPadding = [parts objectAtIndex:1];
bottomPadding = [parts objectAtIndex:0];
leftPadding = [parts objectAtIndex:1];
}
else
{
NSString *onlyValue = [parts objectAtIndex:0];
topPadding = onlyValue;
rightPadding = onlyValue;
bottomPadding = onlyValue;
leftPadding = onlyValue;
}
// only apply the ones where there is no previous direct setting
if (![styles objectForKey:@"padding-top"])
{
[styles setObject:topPadding forKey:@"padding-top"];
}
if (![styles objectForKey:@"padding-right"])
{
[styles setObject:rightPadding forKey:@"padding-right"];
}
if (![styles objectForKey:@"padding-bottom"])
{
[styles setObject:bottomPadding forKey:@"padding-bottom"];
}
if (![styles objectForKey:@"padding-left"])
{
[styles setObject:leftPadding forKey:@"padding-left"];
}
// remove the shorthand
[styles removeObjectForKey:@"padding"];
}
shortHand = [styles objectForKey:@"background"];
if (shortHand && [shortHand isKindOfClass:[NSString class]])
{
// ignore most tokens except background-color
[styles removeObjectForKey:@"background"];
NSCharacterSet *tokenDelimiters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSString *trimmedString = [shortHand stringByTrimmingCharactersInSet:tokenDelimiters];
NSScanner *scanner = [NSScanner scannerWithString:trimmedString];
while (![scanner isAtEnd])
{
NSString *colorName;
if ([scanner scanHTMLColor:NULL HTMLName:&colorName])
{
[styles setObject:colorName forKey:@"background-color"];
break;
}
[scanner scanUpToCharactersFromSet:tokenDelimiters intoString:NULL];
}
}
}
- (void)_addStyleRule:(NSString *)rule withSelector:(NSString*)selectors
{
NSArray *split = [selectors componentsSeparatedByString:@","];
for (NSString *selector in split)
{
NSString *cleanSelector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSMutableDictionary *ruleDictionary = [[rule dictionaryOfCSSStyles] mutableCopy];
// remove !important, we're ignoring these
for (NSString *oneKey in [ruleDictionary allKeys])
{
id value = [ruleDictionary objectForKey:oneKey];
if ([value isKindOfClass:[NSString class]])
{
NSRange rangeOfImportant = [value rangeOfString:@"!important" options:NSCaseInsensitiveSearch];
if (rangeOfImportant.location != NSNotFound)
{
value = [value stringByReplacingCharactersInRange:rangeOfImportant withString:@""];
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[ruleDictionary setObject:value forKey:oneKey];
}
} else if ([value isKindOfClass:[NSArray class]])
{
NSMutableArray *newVal;
for (NSUInteger i = 0; i < [(NSArray*)value count]; ++i)
{
NSString *s = [value objectAtIndex:i];
NSRange rangeOfImportant = [s rangeOfString:@"!important" options:NSCaseInsensitiveSearch];
if (rangeOfImportant.location != NSNotFound)
{
s = [s stringByReplacingCharactersInRange:rangeOfImportant withString:@""];
s = [s stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (!newVal)
{
if ([value isKindOfClass:[NSMutableArray class]])
{
newVal = value;
} else
{
newVal = [value mutableCopy];
}
}
// replace the value that had !important with a version without it
[newVal replaceObjectAtIndex:i withObject:s];
}
}
if (newVal)
{
[ruleDictionary setObject:newVal forKey:oneKey];
}
}
}
// need to uncompress because otherwise we might get shorthands and non-shorthands together
[self _uncompressShorthands:ruleDictionary];
// check if there is a pseudo selector
NSRange colonRange = [cleanSelector rangeOfString:@":"];
NSString *pseudoSelector = nil;
if (colonRange.length==1)
{
pseudoSelector = [cleanSelector substringFromIndex:colonRange.location+1];
cleanSelector = [cleanSelector substringToIndex:colonRange.location];
// prefix all rules with the pseudo-selector
for (NSString *oneRuleKey in [ruleDictionary allKeys])
{
id value = [ruleDictionary objectForKey:oneRuleKey];
// prefix key with the pseudo selector
NSString *prefixedKey = [NSString stringWithFormat:@"%@:%@", pseudoSelector, oneRuleKey];
[ruleDictionary setObject:value forKey:prefixedKey];
[ruleDictionary removeObjectForKey:oneRuleKey];
}
}
NSDictionary *existingRulesForSelector = [_styles objectForKey:cleanSelector];
if (existingRulesForSelector)
{
// substitute new rules over old ones
NSMutableDictionary *tmpDict = [existingRulesForSelector mutableCopy];
// append new rules
[tmpDict addEntriesFromDictionary:ruleDictionary];
// save it
[self _addStyles:tmpDict withSelector:cleanSelector];
}
else
{
[self _addStyles:ruleDictionary withSelector:cleanSelector];
}
}
}
- (void)parseStyleBlock:(NSString*)css
{
NSUInteger braceMarker = 0;
NSInteger braceLevel = 0;
NSString* selector;
NSUInteger length = [css length];
for (NSUInteger i = 0; i < length; i++)
{
unichar c = [css characterAtIndex:i];
if (c == '/')
{
i++;
if (i < length)
{
c = [css characterAtIndex:i];
if (c == '*')
{
// skip comment until closing /
for (; i < length; i++)
{
if ([css characterAtIndex:i] == '/')
{
break;
}
}
if (i < length)
{
braceMarker = i+1;
continue;
}
else
{
// end of string
return;
}
}
else
{
// not a comment
i--;
}
}
}
// An opening brace! It could be the start of a new rule, or it could be a nested brace.
if (c == '{')
{
// If we start a new rule...
if (braceLevel == 0)
{
// Grab the selector and clean up extraneous spaces (we'll process it in a moment)
selector = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
NSMutableArray *cleanSelectorParts = [NSMutableArray array];
for (NSString *partialSelector in selectorParts)
{
if (partialSelector.length)
{
[cleanSelectorParts addObject:partialSelector];
}
}
selector = [cleanSelectorParts componentsJoinedByString:@" "];
// And mark our position so we can grab the rule's CSS when it is closed
braceMarker = i + 1;
}
// Increase the brace level.
braceLevel += 1;
}
// A closing brace!
else if (c == '}')
{
// If we finished a rule...
if (braceLevel == 1)
{
NSString *rule = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
[self _addStyleRule:rule withSelector: selector];
braceMarker = i + 1;
}
// Skip unpaired closing brace
else if (braceLevel < 1) {
braceMarker += 1;
}
braceLevel = MAX(braceLevel-1, 0);
}
}
}
- (void)mergeStylesheet:(DTCSSStylesheet *)stylesheet
{
NSArray *otherStylesheetStyleKeys = stylesheet.orderedSelectors;
for (NSString *oneKey in otherStylesheetStyleKeys)
{
NSDictionary *existingStyles = [_styles objectForKey:oneKey];
NSDictionary *stylesToMerge = [[stylesheet styles] objectForKey:oneKey];
if (existingStyles)
{
NSMutableDictionary *mutableStyles = [existingStyles mutableCopy];
for (NSString *oneStyleKey in stylesToMerge)
{
NSString *mergingStyleString = [stylesToMerge objectForKey:oneStyleKey];
[mutableStyles setObject:mergingStyleString forKey:oneStyleKey];
}
[self _addStyles:mutableStyles withSelector:oneKey];
}
else
{
// nothing to worry
[self _addStyles:stylesToMerge withSelector:oneKey];
}
}
}
- (void)_addStyles:(NSDictionary *)styles withSelector:(NSString *)selector {
[_styles setObject:styles forKey:selector];
if (![_orderedSelectors containsObject:selector])
{
[_orderedSelectors addObject:selector];
[_orderedSelectorWeights setObject:@([self _weightForSelector:selector]) forKey:selector];
}
}
#pragma mark Accessing Style Information
- (NSDictionary *)mergedStyleDictionaryForElement:(DTHTMLElement *)element matchedSelectors:(NSSet * __autoreleasing*)matchedSelectors ignoreInlineStyle:(BOOL)ignoreInlineStyle
{
// We are going to combine all the relevant styles for this tag.
// (Note that when styles are applied, the later styles take precedence,
// so the order in which we grab them matters!)
NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary];
// Get based on element
NSDictionary *byTagName = [self.styles objectForKey:element.name];
if (byTagName)
{
[tmpDict addEntriesFromDictionary:byTagName];
}
// Get based on class(es)
NSString *classString = [element.attributes objectForKey:@"class"];
NSArray *classes = [classString componentsSeparatedByString:@" "];
// Cascaded selectors with more than one part are sorted by specificity
NSMutableArray *matchingCascadingSelectors = [self matchingComplexCascadingSelectorsForElement:element];
[matchingCascadingSelectors sortUsingComparator:^NSComparisonResult(NSString *selector1, NSString *selector2)
{
NSInteger weightForSelector1 = [[self->_orderedSelectorWeights objectForKey:selector1] integerValue];
NSInteger weightForSelector2 = [[self->_orderedSelectorWeights objectForKey:selector2] integerValue];
if (weightForSelector1 == weightForSelector2)
{
weightForSelector1 += [self->_orderedSelectors indexOfObject:selector1];
weightForSelector2 += [self->_orderedSelectors indexOfObject:selector2];
}
if (weightForSelector1 > weightForSelector2)
{
return (NSComparisonResult)NSOrderedDescending;
}
if (weightForSelector1 < weightForSelector2)
{
return (NSComparisonResult)NSOrderedAscending;
}
return (NSComparisonResult)NSOrderedSame;
}];
NSMutableSet *tmpMatchedSelectors;
if (matchedSelectors)
{
tmpMatchedSelectors = [NSMutableSet set];
}
// Apply complex cascading selectors first, then apply most specific selectors
for (NSString *cascadingSelector in matchingCascadingSelectors)
{
NSDictionary *byCascadingSelector = [_styles objectForKey:cascadingSelector];
[tmpDict addEntriesFromDictionary:byCascadingSelector];
[tmpMatchedSelectors addObject:cascadingSelector];
}
// Applied the parameter element's classes last
for (NSString *class in classes)
{
NSString *classRule = [NSString stringWithFormat:@".%@", class];
NSDictionary *byClass = [_styles objectForKey: classRule];
if (byClass)
{
[tmpDict addEntriesFromDictionary:byClass];
[tmpMatchedSelectors addObject:class];
}
NSString *classAndTagRule = [NSString stringWithFormat:@"%@.%@", element.name, class];
NSDictionary *byClassAndName = [_styles objectForKey:classAndTagRule];
if (byClassAndName)
{
[tmpDict addEntriesFromDictionary:byClassAndName];
[tmpMatchedSelectors addObject:classAndTagRule];
}
}
// Get based on id
NSString *idRule = [NSString stringWithFormat:@"#%@", [element.attributes objectForKey:@"id"]];
NSDictionary *byID = [_styles objectForKey:idRule];
if (byID)
{
[tmpDict addEntriesFromDictionary:byID];
[tmpMatchedSelectors addObject:idRule];
}
if (!ignoreInlineStyle)
{
// Get tag's local style attribute
NSString *styleString = [element.attributes objectForKey:@"style"];
if ([styleString length])
{
NSMutableDictionary *localStyles = [[styleString dictionaryOfCSSStyles] mutableCopy];
// need to uncompress because otherwise we might get shorthands and non-shorthands together
[self _uncompressShorthands:localStyles];
[tmpDict addEntriesFromDictionary:localStyles];
}
}
if ([tmpDict count])
{
if (matchedSelectors && [tmpMatchedSelectors count])
{
*matchedSelectors = [tmpMatchedSelectors copy];
}
return tmpDict;
}
else
{
return nil;
}
}
- (NSDictionary *)styles
{
return _styles;
}
- (NSArray *)orderedSelectors
{
return _orderedSelectors;
}
// This looks for cascaded selectors with more than one part to them
- (NSMutableArray *)matchingComplexCascadingSelectorsForElement:(DTHTMLElement *)element
{
__block NSMutableArray *matchedSelectors = [NSMutableArray array];
for (NSString *selector in _orderedSelectors)
{
// We only process the selector if our selector has more than 1 part to it (e.g. ".foo" would be skipped and ".foo .bar" would not)
if (![selector rangeOfString:@" "].length) {
continue;
}
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
if (selectorParts.count < 2)
{
continue;
}
DTHTMLElement *nextElement = element;
// Walking up the hierarchy so start at the right side of the selector and work to the left
// Aside: Manual for loop here is faster than for in with reverseObjectEnumerator
for (NSUInteger j = selectorParts.count; j-- > 0;)
{
NSString *selectorPart = [selectorParts objectAtIndex:j];
BOOL matched = NO;
if (selectorPart.length)
{
while (nextElement != nil)
{
DTHTMLElement *currentElement = nextElement;
//This must be set to advance here, above all of the breaks, so the loop properly advances.
nextElement = currentElement.parentElement;
if ([selectorPart characterAtIndex:0] == '#')
{
// If we're at an id and it doesn't match the current element then the style doesn't apply
NSString *currentElementId = [currentElement.attributes objectForKey:@"id"];
if (currentElementId && [[selectorPart substringFromIndex:1] isEqualToString:currentElementId])
{
matched = YES;
break;
}
} else if ([selectorPart characterAtIndex:0] == '.')
{
NSString *currentElementClassesString = [currentElement.attributes objectForKey:@"class"];
NSArray *currentElementClasses = [currentElementClassesString componentsSeparatedByString:@" "];
for (NSString *currentElementClass in currentElementClasses)
{
if ([currentElementClass isEqualToString:[selectorPart substringFromIndex:1]])
{
matched = YES;
break;
}
}
if (matched)
{
break;
}
} else if ([selectorPart isEqualToString:currentElement.name] && (selectorParts.count > 1))
{
// This condition depends on the "if (selectorParts.count < 2)" conditional above. If that's removed, we must make sure selectorParts
// contains > 1 item for this to be matched (we want the element name alone to be matched last).
matched = YES;
break;
}
// break if the right most portion of the selector doesn't match the target element
if (!matched && ([currentElement isEqual:element])) {
break;
}
}
}
if (!matched)
{
break;
}
//Only match if we really are on the last part of the selector and all other parts have matched so far
if (j == 0)
{
if (matched && ![matchedSelectors containsObject:selector])
{
[matchedSelectors addObject:selector];
}
}
}
}
return matchedSelectors;
}
// This computes the specificity for a given selector
- (NSUInteger)_weightForSelector:(NSString *)selector {
if ((selector == nil) || (selector.length == 0))
{
return 0;
}
NSUInteger weight = 0;
NSArray *selectorParts = [selector componentsSeparatedByString:@" "];
for (NSString *selectorPart in selectorParts)
{
if (selectorPart.length == 0) {
continue;
}
if ([selectorPart characterAtIndex:0] == '#')
{
weight += 100;
} else if ([selectorPart characterAtIndex:0] == '.')
{
weight += 10;
} else {
weight += 1;
}
}
return weight;
}
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone
{
DTCSSStylesheet *newStylesheet = [[DTCSSStylesheet allocWithZone:zone] initWithStylesheet:self];
return newStylesheet;
}
@end
@@ -1,63 +0,0 @@
//
// DTColor+Compatibility.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/9/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE
#import <UIKit/UIColor.h>
/**
Implementations of methods on NSColor/UIColor which are missing on the other platform.
*/
@interface UIColor (HTML)
/**
A quick method to return the alpha component of this UIColor by using the CGColorGetAlpha method.
@returns The floating point alpha value of this UIColor.
*/
- (CGFloat)alphaComponent;
@end
#endif
#if TARGET_OS_OSX
#import <AppKit/NSColor.h>
/**
Methods used to work with HTML representations of colors.
*/
@interface NSColor (HTML)
/**
Return a string hexadecimal representation of this NSColor. Splits the color into components with CGColor methods, re-maps them from percentages in the range 0-255, and returns the RGB color (alpha is stripped) in a six character string.
@returns A CSS hexadecimal NSString specifying this NSColor.
*/
//- (NSString *)htmlHexString;
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7
/**
Converts a CGColorRef into an NSColor by placing each component into an NSColor and pending on the component count to return a grayscale or rgb color. If there are not 2 (grayscale) or 4 (rgba) components the color is from an unsupported color space and nil is returned.
@param cgColor The CGColorRef to convert
@returns An NSColor of this CGColorRef
*/
+ (NSColor *)colorWithCGColor:(CGColorRef)cgColor;
/**
Converts an NSColor into a CGColorRef.
@returns A CGColorRef of this NSColor
*/
- (CGColorRef)CGColor DT_RETURNS_INNER_POINTER;
#endif
@end
#endif
@@ -1,122 +0,0 @@
//
// DTColor+Compatibility.m
// DTCoreText
//
// Created by Oliver Drobnik on 1/9/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTColor+Compatibility.h"
#if TARGET_OS_IPHONE
#import "DTColorFunctions.h"
@implementation UIColor (HTML)
- (CGFloat)alphaComponent
{
return CGColorGetAlpha(self.CGColor);
}
@end
#else
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
#import "DTCoreTextMacros.h"
#import <objc/runtime.h>
static void* DTCoreTextCGColorKey = &DTCoreTextCGColorKey;
#endif // MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
@interface NSColor (DTCoreText)
+ (NSColor *)DTCoreText_colorWithCGColor:(CGColorRef)cgColor;
- (CGColorRef)DTCoreText_CGColor DT_RETURNS_INNER_POINTER;
@end
static void DTCoreTextAddMissingSelector(Class aClass, SEL aSelector, SEL implementationSelector)
{
Method method = class_getInstanceMethod(aClass, aSelector);
if (method == NULL) {
method = class_getInstanceMethod(aClass, implementationSelector);
NSCAssert(method != NULL, @"missing implementation method");
IMP methodImplementation = method_getImplementation(method);
const char *methodTypeEncoding = method_getTypeEncoding(method);
#if !defined(NS_BLOCK_ASSERTIONS)
BOOL rc = class_addMethod(aClass, aSelector, methodImplementation, methodTypeEncoding);
NSCAssert(rc, @"failed to add missing method");
#else
(void) class_addMethod(aClass, aSelector, methodImplementation, methodTypeEncoding);
#endif
}
}
__attribute__((constructor))
static void DTCoreTextNSColorInitialization(void)
{
Class NSColorClass = objc_getClass("NSColor");
Class NSColorMetaClass = object_getClass(NSColorClass);
DTCoreTextAddMissingSelector(NSColorMetaClass, @selector(colorWithCGColor:), @selector(DTCoreText_colorWithCGColor:));
DTCoreTextAddMissingSelector(NSColorClass, @selector(CGColor), @selector(DTCoreText_CGColor));
}
#define colorWithCGColor DTCoreText_colorWithCGColor
#define CGColor DTCoreText_CGColor
#define HTML DTCoreText
#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
@implementation NSColor (HTML)
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
+ (NSColor *)colorWithCGColor:(CGColorRef)cgColor
{
size_t count = CGColorGetNumberOfComponents(cgColor);
const CGFloat *components = CGColorGetComponents(cgColor);
// Grayscale
if (count == 2)
{
return [NSColor colorWithDeviceWhite:components[0] alpha:components[1]];
}
// RGB
else if (count == 4)
{
return [NSColor colorWithDeviceRed:components[0] green:components[1] blue:components[2] alpha:components[3]];
}
// neither grayscale nor rgba
return nil;
}
// From https://gist.github.com/1593255
- (CGColorRef)CGColor
{
CGColorRef color = (__bridge CGColorRef)objc_getAssociatedObject(self, DTCoreTextCGColorKey);
if (color == NULL)
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSColor *selfCopy = [self colorUsingColorSpaceName:NSDeviceRGBColorSpace];
CGFloat colorValues[4];
[selfCopy getRed:&colorValues[0] green:&colorValues[1] blue:&colorValues[2] alpha:&colorValues[3]];
color = CGColorCreate(colorSpace, colorValues);
CGColorSpaceRelease(colorSpace);
objc_setAssociatedObject(self, DTCoreTextCGColorKey, CFBridgingRelease(color), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return color;
}
#endif // MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_7 || MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_7
@end
#endif
@@ -1,34 +0,0 @@
//
// DTColorFunctions.h
// DTCoreText
//
// Created by Oliver Drobnik on 9/9/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
/**
Takes a CSS color string ('333', 'F9FFF9'), determines the RGB values used, and returns a UIColor object of that color.
For each part of the RGB color those numbers for that color are converted to a number using a category on NSString. Then that number is divided by the maximum value, 15 for 3 character strings and 255 for 6 character strings, making the color a percentage and within the range 0.0 and 1.0 that UIColor uses.
@param hexString A CSS hexadecimal color string of length 6 or 3.
@returns A UIColor object generated from the hexadecimal color string with alpha 1.0.
*/
DTColor *DTColorCreateWithHexString(NSString *hexString);
/**
Takes an English string representing a color and maps it to a numeric RGB value as declared by the HTML and CSS specifications (see http://www.w3schools.com/html/html_colornames.asp). Also accepts CSS `#` hexadecimal colors, `rgba`, and `rgb` and does the right thing returning a corresponding UIColor.
If a color begins with a `#` we know that it is a hexadecimal color and send it to colorWithHexString:. If the string is an `rgba()` color declaration the comma delimited r, g, b, and a values are made into percentages and then made into a UIColor which is returned. If the string is an `rgb()` color declaration the same process happens except with an alpha of 1.0.
The last case is that the color string is not a numeric declaration `#`, nor a `rgba` or `rgb` declaration so the CSS color value matching the English string is found in a lookup dictionary and then passed to colorWithHexString: which will make a UIColor out of the hexadecimal string.
@param name The CSS color string that we want to map from a name into an RGB color.
@returns A UIColor object representing the name parameter as numeric values declared by the HTML and CSS specifications, a `rgba()` color, or a `rgb()` color.
*/
DTColor *DTColorCreateWithHTMLName(NSString *name);
/**
Return a string hexadecimal representation of this UIColor. Splits the color into components with CGColor methods, re-maps them from percentages to the range 0-255, and depending on the number of components returns a grayscale (repeating string of two characters) or color RGB (alpha is stripped) six character string. In the event of a non-2 or non-4 component color nil is returned as it is from an unsupported color space.
@returns A CSS hexadecimal NSString specifying this UIColor.
*/
NSString *DTHexStringFromDTColor(DTColor *color);
@@ -1,305 +0,0 @@
//
// DTColorFunctions.m
// DTCoreText
//
// Created by Oliver Drobnik on 9/9/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTColorFunctions.h"
static NSDictionary *colorLookup = nil;
#pragma mark - Private Functions
NSUInteger _integerValueFromHexString(NSString *hexString);
#pragma mark - Implementations
NSUInteger _integerValueFromHexString(NSString *hexString)
{
int result = 0;
sscanf([hexString UTF8String], "%x", &result);
return result;
}
//- (BOOL)isNumeric
//{
// const char *s = [self UTF8String];
//
// for (size_t i=0;i<strlen(s);i++)
// {
// if ((s[i]<'0' || s[i]>'9') && (s[i] != '.'))
// {
// return NO;
// }
// }
//
// return YES;
//}
DTColor *DTColorCreateWithHexString(NSString *hexString)
{
if ([hexString length]!=6 && [hexString length]!=3)
{
return nil;
}
NSUInteger digits = [hexString length]/3;
CGFloat maxValue = (digits==1)?15.0:255.0;
NSUInteger redValue = _integerValueFromHexString([hexString substringWithRange:NSMakeRange(0, digits)]);
NSUInteger greenValue = _integerValueFromHexString([hexString substringWithRange:NSMakeRange(digits, digits)]);
NSUInteger blueValue = _integerValueFromHexString([hexString substringWithRange:NSMakeRange(2*digits, digits)]);
CGFloat red = redValue/maxValue;
CGFloat green = greenValue/maxValue;
CGFloat blue = blueValue/maxValue;
#if TARGET_OS_IPHONE
return [DTColor colorWithRed:red green:green blue:blue alpha:1.0];
#else
return (DTColor *)[NSColor colorWithDeviceRed:red green:green blue:blue alpha:1.0];
#endif
}
DTColor *DTColorCreateWithHTMLName(NSString *name)
{
if ([name hasPrefix:@"#"])
{
return DTColorCreateWithHexString([name substringFromIndex:1]);
}
if ([name hasPrefix:@"rgba"])
{
NSString *rgbaName = [name stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"rgba() "]];
NSArray *rgba = [rgbaName componentsSeparatedByString:@","];
if ([rgba count] != 4)
{
// Incorrect syntax
return nil;
}
CGFloat red = (CGFloat)[[rgba objectAtIndex:0] floatValue] / 255;
CGFloat green = [[rgba objectAtIndex:1] floatValue] / 255;
CGFloat blue = [[rgba objectAtIndex:2] floatValue] / 255;
CGFloat alpha = [[rgba objectAtIndex:3] floatValue];
#if TARGET_OS_IPHONE
return [DTColor colorWithRed:red green:green blue:blue alpha:alpha];
#else
return (DTColor *)[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha];
#endif
}
if([name hasPrefix:@"rgb"])
{
NSString * rgbName = [name stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"rbg() "]];
NSArray* rgb = [rgbName componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@","]];
if ([rgb count] != 3)
{
// Incorrect syntax
return nil;
}
CGFloat red = [[rgb objectAtIndex:0] floatValue] / 255;
CGFloat green = [[rgb objectAtIndex:1] floatValue] / 255;
CGFloat blue = [[rgb objectAtIndex:2] floatValue] / 255;
CGFloat alpha = 1.0;
#if TARGET_OS_IPHONE
return [DTColor colorWithRed:red green:green blue:blue alpha:alpha];
#else
return (DTColor *)[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha];
#endif
}
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
colorLookup = [[NSDictionary alloc] initWithObjectsAndKeys:
@"F0F8FF", @"aliceblue",
@"FAEBD7", @"antiquewhite",
@"00FFFF", @"aqua",
@"7FFFD4", @"aquamarine",
@"F0FFFF", @"azure",
@"F5F5DC", @"beige",
@"FFE4C4", @"bisque",
@"000000", @"black",
@"FFEBCD", @"blanchedalmond",
@"0000FF", @"blue",
@"8A2BE2", @"blueviolet",
@"A52A2A", @"brown",
@"DEB887", @"burlywood",
@"5F9EA0", @"cadetblue",
@"7FFF00", @"chartreuse",
@"D2691E", @"chocolate",
@"FF7F50", @"coral",
@"6495ED", @"cornflowerblue",
@"FFF8DC", @"cornsilk",
@"DC143C", @"crimson",
@"00FFFF", @"cyan",
@"00008B", @"darkblue",
@"008B8B", @"darkcyan",
@"B8860B", @"darkgoldenrod",
@"A9A9A9", @"darkgray",
@"A9A9A9", @"darkgrey",
@"006400", @"darkgreen",
@"BDB76B", @"darkkhaki",
@"8B008B", @"darkmagenta",
@"556B2F", @"darkolivegreen",
@"FF8C00", @"darkorange",
@"9932CC", @"darkorchid",
@"8B0000", @"darkred",
@"E9967A", @"darksalmon",
@"8FBC8F", @"darkseagreen",
@"483D8B", @"darkslateblue",
@"2F4F4F", @"darkslategray",
@"2F4F4F", @"darkslategrey",
@"00CED1", @"darkturquoise",
@"9400D3", @"darkviolet",
@"FF1493", @"deeppink",
@"00BFFF", @"deepskyblue",
@"696969", @"dimgray",
@"696969", @"dimgrey",
@"1E90FF", @"dodgerblue",
@"B22222", @"firebrick",
@"FFFAF0", @"floralwhite",
@"228B22", @"forestgreen",
@"FF00FF", @"fuchsia",
@"DCDCDC", @"gainsboro",
@"F8F8FF", @"ghostwhite",
@"FFD700", @"gold",
@"DAA520", @"goldenrod",
@"808080", @"gray",
@"808080", @"grey",
@"008000", @"green",
@"ADFF2F", @"greenyellow",
@"F0FFF0", @"honeydew",
@"FF69B4", @"hotpink",
@"CD5C5C", @"indianred",
@"4B0082", @"indigo",
@"FFFFF0", @"ivory",
@"F0E68C", @"khaki",
@"E6E6FA", @"lavender",
@"FFF0F5", @"lavenderblush",
@"7CFC00", @"lawngreen",
@"FFFACD", @"lemonchiffon",
@"ADD8E6", @"lightblue",
@"F08080", @"lightcoral",
@"E0FFFF", @"lightcyan",
@"FAFAD2", @"lightgoldenrodyellow",
@"D3D3D3", @"lightgray",
@"D3D3D3", @"lightgrey",
@"90EE90", @"lightgreen",
@"FFB6C1", @"lightpink",
@"FFA07A", @"lightsalmon",
@"20B2AA", @"lightseagreen",
@"87CEFA", @"lightskyblue",
@"778899", @"lightslategray",
@"778899", @"lightslategrey",
@"B0C4DE", @"lightsteelblue",
@"FFFFE0", @"lightyellow",
@"00FF00", @"lime",
@"32CD32", @"limegreen",
@"FAF0E6", @"linen",
@"FF00FF", @"magenta",
@"800000", @"maroon",
@"66CDAA", @"mediumaquamarine",
@"0000CD", @"mediumblue",
@"BA55D3", @"mediumorchid",
@"9370D8", @"mediumpurple",
@"3CB371", @"mediumseagreen",
@"7B68EE", @"mediumslateblue",
@"00FA9A", @"mediumspringgreen",
@"48D1CC", @"mediumturquoise",
@"C71585", @"mediumvioletred",
@"191970", @"midnightblue",
@"F5FFFA", @"mintcream",
@"FFE4E1", @"mistyrose",
@"FFE4B5", @"moccasin",
@"FFDEAD", @"navajowhite",
@"000080", @"navy",
@"FDF5E6", @"oldlace",
@"808000", @"olive",
@"6B8E23", @"olivedrab",
@"FFA500", @"orange",
@"FF4500", @"orangered",
@"DA70D6", @"orchid",
@"EEE8AA", @"palegoldenrod",
@"98FB98", @"palegreen",
@"AFEEEE", @"paleturquoise",
@"D87093", @"palevioletred",
@"FFEFD5", @"papayawhip",
@"FFDAB9", @"peachpuff",
@"CD853F", @"peru",
@"FFC0CB", @"pink",
@"DDA0DD", @"plum",
@"B0E0E6", @"powderblue",
@"800080", @"purple",
@"FF0000", @"red",
@"BC8F8F", @"rosybrown",
@"4169E1", @"royalblue",
@"8B4513", @"saddlebrown",
@"FA8072", @"salmon",
@"F4A460", @"sandybrown",
@"2E8B57", @"seagreen",
@"FFF5EE", @"seashell",
@"A0522D", @"sienna",
@"C0C0C0", @"silver",
@"87CEEB", @"skyblue",
@"6A5ACD", @"slateblue",
@"708090", @"slategray",
@"708090", @"slategrey",
@"FFFAFA", @"snow",
@"00FF7F", @"springgreen",
@"4682B4", @"steelblue",
@"D2B48C", @"tan",
@"008080", @"teal",
@"D8BFD8", @"thistle",
@"FF6347", @"tomato",
@"40E0D0", @"turquoise",
@"EE82EE", @"violet",
@"F5DEB3", @"wheat",
@"FFFFFF", @"white",
@"F5F5F5", @"whitesmoke",
@"FFFF00", @"yellow",
@"9ACD32", @"yellowgreen",
nil];
});
NSString *hexString = [colorLookup objectForKey:[name lowercaseString]];
return DTColorCreateWithHexString(hexString);
}
NSString *DTHexStringFromDTColor(DTColor *color)
{
CGColorRef cgColor = color.CGColor;
size_t count = CGColorGetNumberOfComponents(cgColor);
const CGFloat *components = CGColorGetComponents(cgColor);
static NSString *stringFormat = @"%02x%02x%02x";
// Grayscale
if (count == 2)
{
NSUInteger white = (NSUInteger)(components[0] * (CGFloat)255);
return [NSString stringWithFormat:stringFormat, white, white, white];
}
// RGB
else if (count == 4)
{
return [NSString stringWithFormat:stringFormat, (NSUInteger)(components[0] * (CGFloat)255),
(NSUInteger)(components[1] * (CGFloat)255), (NSUInteger)(components[2] * (CGFloat)255)];
}
// Unsupported color space
return nil;
}
-165
View File
@@ -1,165 +0,0 @@
//
// DTCompatibility.h
// DTCoreText
//
// Created by Oliver Letterer on 09.04.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import <Availability.h>
#import <TargetConditionals.h>
#import <Foundation/Foundation.h>
#pragma mark - iOS
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
// Compatibility Aliases
#define DTColor UIColor
#define DTImage UIImage
#define DTFont UIFont
// Edge Insets
#define DTEdgeInsets UIEdgeInsets
#define DTEdgeInsetsMake(top, left, bottom, right) UIEdgeInsetsMake(top, left, bottom, right)
// NS-style text attributes are possible with iOS SDK 6.0 or higher
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_5_1
#define DTCORETEXT_SUPPORT_NS_ATTRIBUTES 1
#endif
// NSParagraphStyle supports tabs as of iOS SDK 7.0 or higher
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
#define DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS 1
#endif
// iOS before 5.0 has leak in CoreText replacing attributes
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_5_0
#define DTCORETEXT_NEEDS_ATTRIBUTE_REPLACEMENT_LEAK_FIX 1
#endif
// iOS 7 bug (rdar://14684188) workaround, can be removed once this bug is fixed
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1
#define DTCORETEXT_FIX_14684188 1
#endif
// use NSURLSession if NSURLConnection is deprecated
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0
#define DTCORETEXT_USES_NSURLSESSION 1
#endif
// constant for checking for iOS 6
#define DTNSFoundationVersionNumber_iOS_6_0 992.00
// constant for checking for iOS 7
#define DTNSFoundationVersionNumber_iOS_7_0 1047.00
// runtime-check if NS-style attributes are allowed
static inline BOOL DTCoreTextModernAttributesPossible(void);
static inline BOOL DTCoreTextModernAttributesPossible(void)
{
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
if (floor(NSFoundationVersionNumber) >= DTNSFoundationVersionNumber_iOS_6_0)
{
return YES;
}
#endif
return NO;
}
// runtime-check if CoreText draws underlines
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs(void);
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs(void)
{
if (floor(NSFoundationVersionNumber) >= DTNSFoundationVersionNumber_iOS_7_0)
{
return YES;
}
return NO;
}
#if TARGET_CPU_ARM64 || TARGET_CPU_X86_64
#define DTNSNumberFromCGFloat(x) [NSNumber numberWithDouble:x]
#else
#define DTNSNumberFromCGFloat(x) [NSNumber numberWithFloat:x]
#endif
#endif
#pragma mark - Mac
#if !TARGET_OS_IPHONE
#import <AppKit/AppKit.h>
// Compatibility Aliases
#define DTColor NSColor
#define DTImage NSImage
#define DTFont NSFont
// Edge Insets
#define DTEdgeInsets NSEdgeInsets
#define DTEdgeInsetsMake(top, left, bottom, right) NSEdgeInsetsMake(top, left, bottom, right)
// Mac supports NS-Style Text Attributes since 10.0
#define DTCORETEXT_SUPPORT_NS_ATTRIBUTES 1
#define DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS 1
// theoretically MacOS before 10.8 might have a leak in CoreText replacing attributes
#if __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_7
#define DTCORETEXT_NEEDS_ATTRIBUTE_REPLACEMENT_LEAK_FIX 1
#endif
// use NSURLSession if NSURLConnection is deprecated
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_11
#define DTCORETEXT_USES_NSURLSESSION 1
#endif
// NSValue has sizeValue on Mac, CGSizeValue on iOS
#define CGSizeValue sizeValue
// String functions named differently on Mac
static inline NSString *NSStringFromCGRect(const CGRect rect);
static inline NSString *NSStringFromCGRect(const CGRect rect)
{
return NSStringFromRect(NSRectFromCGRect(rect));
}
static inline NSString *NSStringFromCGSize(const CGSize size);
static inline NSString *NSStringFromCGSize(const CGSize size)
{
return NSStringFromSize(NSSizeFromCGSize(size));
}
static inline NSString *NSStringFromCGPoint(const CGPoint point);
static inline NSString *NSStringFromCGPoint(const CGPoint point)
{
return NSStringFromPoint(NSPointFromCGPoint(point));
}
// runtime-check if NS-style attributes are allowed
static inline BOOL DTCoreTextModernAttributesPossible();
static inline BOOL DTCoreTextModernAttributesPossible()
{
return YES;
}
// runtime-check if CoreText draws underlines
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs();
static inline BOOL DTCoreTextDrawsUnderlinesWithGlyphs()
{
return NO;
}
#define DTNSNumberFromCGFloat(x) [NSNumber numberWithDouble:x]
#endif
// this enables generic ceil, floor, abs, round functions that work for 64 and 32 bit
#include <tgmath.h>
-96
View File
@@ -1,96 +0,0 @@
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#if TARGET_OS_IPHONE
#import <CoreText/CoreText.h>
#elif TARGET_OS_MAC
#import <ApplicationServices/ApplicationServices.h>
#endif
// global constants
#import "DTCoreTextMacros.h"
#import "DTCoreTextConstants.h"
#import "DTCompatibility.h"
#import "DTColor+Compatibility.h"
#import "DTImage+HTML.h"
// common utilities
#if TARGET_OS_IPHONE
#import "DTCoreTextFunctions.h"
#endif
#import "DTColorFunctions.h"
// common classes
#import "DTCSSListStyle.h"
#import "DTTextBlock.h"
#import "DTCSSStylesheet.h"
#import "DTCoreTextFontDescriptor.h"
#import "DTCoreTextParagraphStyle.h"
#import "DTHTMLAttributedStringBuilder.h"
#import "DTHTMLElement.h"
#import "DTAnchorHTMLElement.h"
#import "DTBreakHTMLElement.h"
#import "DTListItemHTMLElement.h"
#import "DTHorizontalRuleHTMLElement.h"
#import "DTStylesheetHTMLElement.h"
#import "DTTextAttachmentHTMLElement.h"
#import "DTTextHTMLElement.h"
#import "DTHTMLWriter.h"
#import "NSCharacterSet+HTML.h"
#import "NSCoder+DTCompatibility.h"
#import "NSDictionary+DTCoreText.h"
#import "NSAttributedString+HTML.h"
#import "NSAttributedString+SmallCaps.h"
#import "NSAttributedString+DTCoreText.h"
#import "NSMutableAttributedString+HTML.h"
#import "NSMutableString+HTML.h"
#import "NSScanner+HTML.h"
#import "NSString+CSS.h"
#import "NSString+HTML.h"
#import "NSString+Paragraphs.h"
#import "NSNumber+RomanNumerals.h"
// parsing classes
#import "DTHTMLParserNode.h"
#import "DTHTMLParserTextNode.h"
// text attachment cluster
#import "DTTextAttachment.h"
#import "DTDictationPlaceholderTextAttachment.h"
#import "DTIframeTextAttachment.h"
#import "DTImageTextAttachment.h"
#import "DTObjectTextAttachment.h"
#import "DTVideoTextAttachment.h"
#import "NSAttributedStringRunDelegates.h"
#import "DTCoreTextGlyphRun.h"
#import "DTCoreTextLayoutFrame.h"
#import "DTCoreTextLayoutFrame+Cursor.h"
#import "DTCoreTextLayoutLine.h"
#import "DTCoreTextLayouter.h"
// TARGET_OS_IPHONE is both tvOS and iOS
#if TARGET_OS_IPHONE
#import "DTLazyImageView.h"
#import "DTLinkButton.h"
#import "DTAttributedLabel.h"
#import "DTAttributedTextCell.h"
#import "DTAttributedTextContentView.h"
#import "DTAttributedTextView.h"
#import "DTCoreTextFontCollection.h"
#import "DTDictationPlaceholderView.h"
#import "UIFont+DTCoreText.h"
#import "DTAccessibilityElement.h"
#import "DTAccessibilityViewProxy.h"
#import "DTCoreTextLayoutFrameAccessibilityElementGenerator.h"
#endif
@@ -1,203 +0,0 @@
#import "DTCompatibility.h"
// unicode characters
#define UNICODE_OBJECT_PLACEHOLDER @"\ufffc"
#define UNICODE_LINE_FEED @"\u2028"
// unicode spaces used in CharacterSet.ignorableWhitespaceCharacterSet
#define UNICODE_NON_BREAKING_SPACE @"\u00a0"
#define UNICODE_OGHAM_SPACE_MARK @"\u1680"
#define UNICODE_MONGOLIAN_VOWEL_SEPARATOR @"\u180e"
#define UNICODE_EN_QUAD @"\u2000"
#define UNICODE_EM_QUAD @"\u2001"
#define UNICODE_EN_SPACE @"\u2002"
#define UNICODE_EM_SPACE @"\u2003"
#define UNICODE_THREE_PER_EM_SPACE @"\u2004"
#define UNICODE_FOUR_PER_EM_SPACE @"\u2005"
#define UNICODE_SIX_PER_EM_SPACE @"\u2006"
#define UNICODE_FIGURE_SPACE @"\u2007"
#define UNICODE_PUNCTUATION_SPACE @"\u2008"
#define UNICODE_THIN_SPACE @"\u2009"
#define UNICODE_HAIR_SPACE @"\u200a"
#define UNICODE_ZERO_WIDTH_SPACE @"\u200b"
#define UNICODE_NARROW_NO_BREAK_SPACE @"\u202f"
#define UNICODE_MEDIUM_MATHEMATICAL_SPACE @"\u205f"
#define UNICODE_IDEOGRAPHIC_SPACE @"\u3000"
#define UNICODE_ZERO_WIDTH_NO_BREAK_SPACE @"\ufeff"
// standard options
#if TARGET_OS_IPHONE
extern NSString * const NSBaseURLDocumentOption;
extern NSString * const NSTextEncodingNameDocumentOption;
extern NSString * const NSTextSizeMultiplierDocumentOption;
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_7_0
extern NSString * const NSAttachmentAttributeName;
#endif
#endif
// custom options
extern NSString * const DTMaxImageSize;
extern NSString * const DTDefaultFontFamily;
extern NSString * const DTDefaultFontName;
extern NSString * const DTDefaultFontSize;
extern NSString * const DTDefaultFontDescriptor;
extern NSString * const DTDefaultTextColor;
extern NSString * const DTDefaultLinkColor;
extern NSString * const DTDefaultLinkDecoration;
extern NSString * const DTDefaultLinkHighlightColor;
extern NSString * const DTDefaultTextAlignment;
extern NSString * const DTDefaultLineHeightMultiplier;
extern NSString * const DTDefaultLineHeightMultiplier;
extern NSString * const DTDefaultFirstLineHeadIndent;
extern NSString * const DTDefaultHeadIndent;
extern NSString * const DTDefaultStyleSheet;
extern NSString * const DTUseiOS6Attributes;
extern NSString * const DTWillFlushBlockCallBack;
extern NSString * const DTProcessCustomHTMLAttributes;
extern NSString * const DTIgnoreInlineStylesOption;
extern NSString * const DTDocumentPreserveTrailingSpaces;
// attributed string attribute constants
extern NSString * const DTTextListsAttribute;
extern NSString * const DTAttachmentParagraphSpacingAttribute;
extern NSString * const DTLinkAttribute;
extern NSString * const DTLinkHighlightColorAttribute;
extern NSString * const DTAnchorAttribute;
extern NSString * const DTGUIDAttribute;
extern NSString * const DTHeaderLevelAttribute;
extern NSString * const DTStrikeOutAttribute;
extern NSString * const DTBackgroundColorAttribute;
extern NSString * const DTShadowsAttribute;
extern NSString * const DTHorizontalRuleStyleAttribute;
extern NSString * const DTTextBlocksAttribute;
extern NSString * const DTFieldAttribute;
extern NSString * const DTCustomAttributesAttribute;
extern NSString * const DTAscentMultiplierAttribute;
extern NSString * const DTBackgroundStrokeColorAttribute;
extern NSString * const DTBackgroundStrokeWidthAttribute;
extern NSString * const DTBackgroundCornerRadiusAttribute;
extern NSString * const DTArchivingAttribute;
// field constants
extern NSString * const DTListPrefixField;
// iOS 6 compatibility
extern BOOL ___useiOS6Attributes;
// exceptions
extern NSString * const DTCoreTextFontDescriptorException;
// macros
#define IS_WHITESPACE(_c) (_c == ' ' || _c == '\t' || _c == 0xA || _c == 0xB || _c == 0xC || _c == 0xD || _c == 0x85)
// types
/**
DTHTMLElement display style
*/
typedef NS_ENUM(NSUInteger, DTHTMLElementDisplayStyle)
{
/**
The element is inline text
*/
DTHTMLElementDisplayStyleInline = 0, // default
/**
The element is not displayed
*/
DTHTMLElementDisplayStyleNone,
/**
The element is a block
*/
DTHTMLElementDisplayStyleBlock,
/**
The element is an item in a list
*/
DTHTMLElementDisplayStyleListItem,
/**
The element is a table
*/
DTHTMLElementDisplayStyleTable,
};
/**
DTHTMLElement floating style
*/
typedef NS_ENUM(NSUInteger, DTHTMLElementFloatStyle)
{
/**
The element does not float
*/
DTHTMLElementFloatStyleNone = 0,
/**
The element should float left-aligned
*/
DTHTMLElementFloatStyleLeft,
/**
The element should float right-aligned
*/
DTHTMLElementFloatStyleRight
};
/**
DTHTMLElement font variants
*/
typedef NS_ENUM(NSUInteger, DTHTMLElementFontVariant)
{
/**
The element inherits the font variant
*/
DTHTMLElementFontVariantInherit = 0,
/**
The element uses the normal font variant
*/
DTHTMLElementFontVariantNormal,
/**
The element should display in small caps
*/
DTHTMLElementFontVariantSmallCaps
};
/**
The algorithm that DTCoreTextLayoutFrame uses for positioning lines
*/
typedef NS_ENUM(NSUInteger, DTCoreTextLayoutFrameLinePositioningOptions)
{
/**
The line positioning algorithm is similar to how Safari positions lines
*/
DTCoreTextLayoutFrameLinePositioningOptionAlgorithmWebKit = 1,
/**
The line positioning algorithm is how it was before the implementation of DTCoreTextLayoutFrameLinePositioningOptionAlgorithmWebKit
*/
DTCoreTextLayoutFrameLinePositioningOptionAlgorithmLegacy = 2
};
// layouting
// the value to use if the width is unknown
#define CGFLOAT_WIDTH_UNKNOWN 16777215.0f
// the value to use if the height is unknown
#define CGFLOAT_HEIGHT_UNKNOWN 16777215.0f
@@ -1,71 +0,0 @@
#import "DTCoreTextConstants.h"
// standard options
#if TARGET_OS_IPHONE
NSString * const NSBaseURLDocumentOption = @"NSBaseURLDocumentOption";
NSString * const NSTextEncodingNameDocumentOption = @"NSTextEncodingNameDocumentOption";
NSString * const NSTextSizeMultiplierDocumentOption = @"NSTextSizeMultiplierDocumentOption";
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_7_0
NSString * const NSAttachmentAttributeName = @"NSAttachmentAttributeName";
#endif
#endif
// custom options
NSString * const DTMaxImageSize = @"DTMaxImageSize";
NSString * const DTDefaultFontFamily = @"DTDefaultFontFamily";
NSString * const DTDefaultFontName = @"DTDefaultFontName";
NSString * const DTDefaultFontSize = @"DTDefaultFontSize";
NSString * const DTDefaultFontDescriptor = @"DTDefaultFontDescriptor";
NSString * const DTDefaultTextColor = @"DTDefaultTextColor";
NSString * const DTDefaultLinkColor = @"DTDefaultLinkColor";
NSString * const DTDefaultLinkHighlightColor = @"DTDefaultLinkHighlightColor";
NSString * const DTDefaultLinkDecoration = @"DTDefaultLinkDecoration";
NSString * const DTDefaultTextAlignment = @"DTDefaultTextAlignment";
NSString * const DTDefaultLineHeightMultiplier = @"DTDefaultLineHeightMultiplier";
NSString * const DTDefaultFirstLineHeadIndent = @"DTDefaultFirstLineHeadIndent";
NSString * const DTDefaultHeadIndent = @"DTDefaultHeadIndent";
NSString * const DTDefaultStyleSheet = @"DTDefaultStyleSheet";
NSString * const DTUseiOS6Attributes = @"DTUseiOS6Attributes";
NSString * const DTWillFlushBlockCallBack = @"DTWillFlushBlockCallBack";
NSString * const DTProcessCustomHTMLAttributes = @"DTProcessCustomHTMLAttributes";
NSString * const DTIgnoreInlineStylesOption = @"DTIgnoreInlineStyles";
NSString * const DTDocumentPreserveTrailingSpaces = @"DTDocumentPreserveTrailingSpaces";
// attributed string attribute constants
NSString * const DTTextListsAttribute = @"DTTextLists";
NSString * const DTAttachmentParagraphSpacingAttribute = @"DTAttachmentParagraphSpacing";
NSString * const DTLinkAttribute = @"NSLink";
NSString * const DTLinkHighlightColorAttribute = @"DTLinkHighlightColor";
NSString * const DTAnchorAttribute = @"DTAnchor";
NSString * const DTGUIDAttribute = @"DTGUID";
NSString * const DTHeaderLevelAttribute = @"DTHeaderLevel";
NSString * const DTStrikeOutAttribute = @"DTStrikethrough";
NSString * const DTBackgroundColorAttribute = @"DTBackgroundColor";
NSString * const DTShadowsAttribute = @"DTShadows";
NSString * const DTHorizontalRuleStyleAttribute = @"DTHorizontalRuleStyle";
NSString * const DTTextBlocksAttribute = @"DTTextBlocks";
NSString * const DTFieldAttribute = @"DTField";
NSString * const DTCustomAttributesAttribute = @"DTCustomAttributes";
NSString * const DTAscentMultiplierAttribute = @"DTAscentMultiplierAttribute";
NSString * const DTBackgroundStrokeColorAttribute = @"DTBackgroundStrokeColor";
NSString * const DTBackgroundStrokeWidthAttribute = @"DTBackgroundStrokeWidth";
NSString * const DTBackgroundCornerRadiusAttribute = @"DTBackgroundCornerRadius";
NSString * const DTArchivingAttribute = @"DTArchivingAttribute";
// field constants
NSString * const DTListPrefixField = @"{listprefix}";
// iOS 6 compatibility
BOOL ___useiOS6Attributes = NO; // this gets set globally by DTHTMLAttributedStringBuilder
// exceptions
NSString * const DTCoreTextFontDescriptorException = @"DTCoreTextFontDescriptorException";
@@ -1,54 +0,0 @@
//
// DTCoreTextFontCollection.h
// DTCoreText
//
// Created by Oliver Drobnik on 5/23/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import <Foundation/Foundation.h>
@class DTCoreTextFontDescriptor;
/**
Class representing a collection of fonts
*/
@interface DTCoreTextFontCollection : NSObject
/**
@name Creating Font Collections
*/
/**
Creates a font collection with all available fonts on the system
*/
+ (DTCoreTextFontCollection *)availableFontsCollection;
/**
@name Getting Information about Font Collections
*/
/**
The font family names that occur in the receiver's list of fonts
*/
- (NSArray *)fontFamilyNames;
/**
The font descriptors describing all fonts in the receiver's font collection
*/
- (NSArray *)fontDescriptors;
/**
@name Searching for Fonts
*/
/**
The font descriptor describing a font in the receiver's collection that matches a given descriptor
@param descriptor The font descriptor to search for
@returns The first found font descriptor in the font collection
*/
- (DTCoreTextFontDescriptor *)matchingFontDescriptorForFontDescriptor:(DTCoreTextFontDescriptor *)descriptor;
@end
@@ -1,162 +0,0 @@
//
// DTCoreTextFontCollection.m
// DTCoreText
//
// Created by Oliver Drobnik on 5/23/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTCoreTextFontCollection.h"
#import "DTCoreTextFontDescriptor.h"
#if TARGET_OS_IPHONE
#import <CoreText/CoreText.h>
#elif TARGET_OS_MAC
#import <ApplicationServices/ApplicationServices.h>
#endif
@interface DTCoreTextFontCollection ()
@property (nonatomic, strong) NSArray *fontDescriptors;
@property (nonatomic, strong) NSCache *fontMatchCache;
- (id)initWithAvailableFonts;
@end
static DTCoreTextFontCollection *_availableFontsCollection = nil;
@implementation DTCoreTextFontCollection
{
NSArray *_fontDescriptors;
NSCache *_fontMatchCache;
}
+ (DTCoreTextFontCollection *)availableFontsCollection
{
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
_availableFontsCollection = [[DTCoreTextFontCollection alloc] initWithAvailableFonts];
});
return _availableFontsCollection;
}
- (id)initWithAvailableFonts
{
self = [super init];
if (self)
{
}
return self;
}
- (DTCoreTextFontDescriptor *)matchingFontDescriptorForFontDescriptor:(DTCoreTextFontDescriptor *)descriptor
{
DTCoreTextFontDescriptor *firstMatch = nil;
NSString *cacheKey = [NSString stringWithFormat:@"fontFamily BEGINSWITH[cd] %@ and boldTrait == %d and italicTrait == %d", descriptor.fontFamily, descriptor.boldTrait, descriptor.italicTrait];
// try cache
firstMatch = [self.fontMatchCache objectForKey:cacheKey];
if (firstMatch)
{
DTCoreTextFontDescriptor *retMatch = [firstMatch copy];
retMatch.pointSize = descriptor.pointSize;
return retMatch;
}
// need to search
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"fontFamily BEGINSWITH[cd] %@ and boldTrait == %d and italicTrait == %d", descriptor.fontFamily, descriptor.boldTrait, descriptor.italicTrait];
NSArray *matchingDescriptors = [self.fontDescriptors filteredArrayUsingPredicate:predicate];
//NSLog(@"%@", matchingDescriptors);
if ([matchingDescriptors count])
{
firstMatch = [matchingDescriptors objectAtIndex:0];
[self.fontMatchCache setObject:firstMatch forKey:cacheKey];
DTCoreTextFontDescriptor *retMatch = [firstMatch copy];
retMatch.pointSize = descriptor.pointSize;
return retMatch;
}
return nil;
}
#pragma mark Properties
- (NSArray *)fontDescriptors
{
if (!_fontDescriptors)
{
CTFontCollectionRef fonts = CTFontCollectionCreateFromAvailableFonts(NULL);
CFArrayRef matchingFonts = CTFontCollectionCreateMatchingFontDescriptors(fonts);
if (matchingFonts)
{
// convert all to our objects
NSMutableArray *tmpArray = [[NSMutableArray alloc] init];
for (NSInteger i=0; i<CFArrayGetCount(matchingFonts); i++)
{
CTFontDescriptorRef fontDesc = CFArrayGetValueAtIndex(matchingFonts, i);
DTCoreTextFontDescriptor *desc = [[DTCoreTextFontDescriptor alloc] initWithCTFontDescriptor:fontDesc];
[tmpArray addObject:desc];
}
CFRelease(matchingFonts);
self.fontDescriptors = tmpArray;
}
CFRelease(fonts);
}
return _fontDescriptors;
}
- (NSCache *)fontMatchCache
{
if (!_fontMatchCache)
{
_fontMatchCache = [[NSCache alloc] init];
}
return _fontMatchCache;
}
- (NSArray *)fontFamilyNames
{
NSMutableArray *tmpArray = [NSMutableArray array];
for (DTCoreTextFontDescriptor *oneDescriptor in [self fontDescriptors])
{
NSString *familyName = oneDescriptor.fontFamily;
if (![tmpArray containsObject:familyName])
{
[tmpArray addObject:familyName];
}
}
return [tmpArray sortedArrayUsingSelector:@selector(compare:)];
}
@synthesize fontDescriptors = _fontDescriptors;
@synthesize fontMatchCache = _fontMatchCache;
@end
@@ -1,228 +0,0 @@
//
// DTCoreTextFontDescriptor.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/26/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>
/**
This class describes the attributes of a font. It is used to represent fonts throughout the parsing and when needed is able to generated matching `CTFont` instances.
*/
@interface DTCoreTextFontDescriptor : NSObject <NSCopying, NSCoding>
/**
@name Creating Font Descriptors
*/
/**
Convenience method to create a font descriptor from a font attributes dictionary
@param attributes The dictionary of font attributes
@returns An initialized font descriptor
*/
+ (DTCoreTextFontDescriptor *)fontDescriptorWithFontAttributes:(NSDictionary *)attributes;
/**
Convenience method for creates a font descriptor from a Core Text font
@param ctFont The Core Text font
@returns An initialized font descriptor
*/
+ (DTCoreTextFontDescriptor *)fontDescriptorForCTFont:(CTFontRef)ctFont;
/**
Creates a font descriptor from a font attributes dictionary
@param attributes The dictionary of font attributes
@returns An initialized font descriptor
*/
- (id)initWithFontAttributes:(NSDictionary *)attributes;
/**
Creates a font descriptor from a Core Text font descriptor
@param ctFontDescriptor The Core Text font descriptor
@returns An initialized font descriptor
*/
- (id)initWithCTFontDescriptor:(CTFontDescriptorRef)ctFontDescriptor;
/**
Creates a font descriptor from a Core Text font
@param ctFont The Core Text font
@returns An initialized font descriptor
*/
- (id)initWithCTFont:(CTFontRef)ctFont;
/**
@name Creating Fonts from Font Descriptors
*/
/**
Creates a `CTFont` matching the receiver's attribute
@returns a +1 owning reference of a Core Text font
*/
- (CTFontRef)newMatchingFont;
/**
@name Specifying Font Attributes
*/
/**
Sets the font attributes from a dictionary
@param newAttributes The font attributes dictionary
*/
- (void)setFontAttributes:(NSDictionary *)newAttributes;
/**
Retrieves a dictionary of font attributes
*/
- (NSDictionary *)fontAttributes;
/**
The font family name of the described font
*/
@property (nonatomic, copy) NSString *fontFamily;
/**
The font name of the described font
*/
@property (nonatomic, copy) NSString *fontName;
/**
The point size of the described font
*/
@property (nonatomic) CGFloat pointSize;
/**
Whether the described font has the bold trait
*/
@property (nonatomic) BOOL boldTrait;
/**
Whether the described font has the italic trait
*/
@property (nonatomic) BOOL italicTrait;
/**
Whether the described font has the expanded trait
*/
@property (nonatomic) BOOL expandedTrait;
/**
Whether the described font has the condensed trait
*/
@property (nonatomic) BOOL condensedTrait;
/**
Whether the described font has the monospace trait
*/
@property (nonatomic) BOOL monospaceTrait;
/**
Whether the described font has the vertical trait
*/
@property (nonatomic) BOOL verticalTrait;
/**
Whether the described font is optimized for use in User Interfaces
*/
@property (nonatomic) BOOL UIoptimizedTrait;
/**
The symbolic traits of the receiver
*/
@property (nonatomic) CTFontSymbolicTraits symbolicTraits;
/**
The stylistic class of the receiver
*/
@property (nonatomic) CTFontStylisticClass stylisticClass;
/**
`YES` if the small caps style is enabled, `NO` if not
*/
@property (nonatomic) BOOL smallCapsFeature;
/**
Determining if the font described by the receiver has native small caps support
@returns `YES` if this font supports native small caps
*/
- (BOOL)supportsNativeSmallCaps;
/**
Working with CSS
*/
/**
The CSS style sheet representation of the receiver
@returns A CSS style string
*/
- (NSString *)cssStyleRepresentation;
/**
@name Global Font Overriding
*/
/**
A call to the method is ideally placed into your app delegate. This loads all available system fonts into a look up table to allow DTCoreText to quickly find a specific combination of font-family and italic and bold attributes. Please refer to the [Programming Guide](../docs/Programming%20Guide.html) for information when you should be using this.
Calling this does not replace entries already existing in the lookup table, for example loaded from the `DTCoreTextFontOverrides.plist` included in the app bundle.
*/
+ (void)asyncPreloadFontLookupTable;
/**
Sets the font family to use if the font family in a font descriptor is invalid.
The fallback font family cannot be `nil` and must be a valid font family. The default is **Times New Roman**.
@param fontFamily The font family
*/
+ (void)setFallbackFontFamily:(NSString *)fontFamily;
/**
Returns the font family to use if the font family in a font descriptor is invalid. The default is **Times New Roman**.
@returns The font family
*/
+ (NSString *)fallbackFontFamily;
/**
Sets the global font name override to use when encountering a font family with given bold and italic attributes.
@param fontName The font name to use
@param fontFamily The font family to use this for
@param bold The bold trait
@param italic The italic trait
*/
+ (void)setOverrideFontName:(NSString *)fontName forFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
/**
Retrieves the global font name override for a given font family with bold and italic traits.
@param fontFamily The font family to retrieve the override for
@param bold The bold trait
@param italic The italic trait
@returns The font name to use for this combination of parameters
*/
+ (NSString *)overrideFontNameforFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
/**
Sets the global font name override to use when encountering small caps text in a font family with given bold and italic attributes.
@param fontName The font name to use
@param fontFamily The font family to use this for
@param bold The bold trait
@param italic The italic trait
*/
+ (void)setSmallCapsFontName:(NSString *)fontName forFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
/**
Retrieves the global font name override to use for small caps text for a given font family with bold and italic traits.
@param fontFamily The font family to retrieve the override for
@param bold The bold trait
@param italic The italic trait
@returns The font name to use for this combination of parameters
*/
+ (NSString *)smallCapsFontNameforFontFamily:(NSString *)fontFamily bold:(BOOL)bold italic:(BOOL)italic;
@end
File diff suppressed because it is too large Load Diff
@@ -1,69 +0,0 @@
//
// DTCoreTextFunctions.h
// DTCoreText
//
// Created by Oliver Drobnik on 21.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import <CoreText/CoreText.h>
#import "DTCompatibility.h"
#if TARGET_OS_OSX
#import <AppKit/AppKit.h>
#endif
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
/**
Creates a CTFont from a UIFont
@param font The `UIFont`
@returns The matching CTFont
*/
CTFontRef DTCTFontCreateWithUIFont(UIFont *font);
#endif
/**
Converts an NSLineBreakMode into CoreText line truncation type
*/
CTLineTruncationType DTCTLineTruncationTypeFromNSLineBreakMode(NSLineBreakMode lineBreakMode);
/**
Rounds the passed value according to the specified content scale.
With contentScale 1 the results are identical to roundf, with Retina content scale 2 the results are multiples of 0.5.
*/
CGFloat DTRoundWithContentScale(CGFloat value, CGFloat contentScale);
/**
Rounds up the passed value according to the specified content scale.
With contentScale 1 the results are identical to roundf, with Retina content scale 2 the results are multiples of 0.5.
*/
CGFloat DTCeilWithContentScale(CGFloat value, CGFloat contentScale);
/**
Rounds down the passed value according to the sspecifiedcontent scale.
With contentScale 1 the results are identical to roundf, with Retina content scale 2 the results are multiples of 0.5.
*/
CGFloat DTFloorWithContentScale(CGFloat value, CGFloat contentScale);
#pragma mark - Alignment Conversion
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
/**
Converts from NSTextAlignment to CTTextAligment
*/
CTTextAlignment DTNSTextAlignmentToCTTextAlignment(NSTextAlignment nsTextAlignment);
/**
Converts from CTTextAlignment to NSTextAligment
*/
NSTextAlignment DTNSTextAlignmentFromCTTextAlignment(CTTextAlignment ctTextAlignment);
#endif
@@ -1,195 +0,0 @@
//
// DTCoreTextFunctions.m
// DTCoreText
//
// Created by Oliver Drobnik on 21.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTCoreTextFunctions.h"
#import <DTFoundation/DTLog.h>
#if TARGET_OS_IPHONE
CTFontRef DTCTFontCreateWithUIFont(UIFont *font)
{
return CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
}
#endif
CTLineTruncationType DTCTLineTruncationTypeFromNSLineBreakMode(NSLineBreakMode lineBreakMode)
{
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
switch (lineBreakMode)
{
case UILineBreakModeHeadTruncation:
return kCTLineTruncationStart;
case UILineBreakModeMiddleTruncation:
return kCTLineTruncationMiddle;
default:
return kCTLineTruncationEnd;
}
#else
switch (lineBreakMode)
{
case NSLineBreakByTruncatingHead:
return kCTLineTruncationStart;
case NSLineBreakByTruncatingMiddle:
return kCTLineTruncationMiddle;
default:
return kCTLineTruncationEnd;
}
#endif
}
CGFloat DTRoundWithContentScale(CGFloat value, CGFloat contentScale)
{
return round(value*contentScale)/contentScale;
}
CGFloat DTCeilWithContentScale(CGFloat value, CGFloat contentScale)
{
return ceil(value*contentScale)/contentScale;
}
CGFloat DTFloorWithContentScale(CGFloat value, CGFloat contentScale)
{
return floor(value*contentScale)/contentScale;
}
#pragma mark - Alignment Functions
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
CTTextAlignment DTNSTextAlignmentToCTTextAlignment(NSTextAlignment nsTextAlignment)
{
switch (nsTextAlignment)
{
#if TARGET_OS_IPHONE
case NSTextAlignmentLeft:
{
return kCTTextAlignmentLeft;
}
case NSTextAlignmentRight:
{
return kCTTextAlignmentRight;
}
case NSTextAlignmentCenter:
{
return kCTTextAlignmentCenter;
}
case NSTextAlignmentJustified:
{
return kCTTextAlignmentJustified;
}
case NSTextAlignmentNatural:
{
return kCTTextAlignmentNatural;
}
#else
case NSLeftTextAlignment:
{
return kCTTextAlignmentLeft;
}
case NSRightTextAlignment:
{
return kCTTextAlignmentRight;
}
case NSCenterTextAlignment:
{
return kCTTextAlignmentCenter;
}
case NSJustifiedTextAlignment:
{
return kCTTextAlignmentJustified;
}
case NSNaturalTextAlignment:
{
return kCTTextAlignmentNatural;
}
#endif
default:
{
DTLogError(@"Unknown alignment %d", (int)nsTextAlignment);
return 0;
}
}
}
NSTextAlignment DTNSTextAlignmentFromCTTextAlignment(CTTextAlignment ctTextAlignment)
{
#if TARGET_OS_IPHONE
switch (ctTextAlignment)
{
case kCTTextAlignmentLeft:
{
return NSTextAlignmentLeft;
}
case kCTTextAlignmentRight:
{
return NSTextAlignmentRight;
}
case kCTTextAlignmentCenter:
{
return NSTextAlignmentCenter;
}
case kCTTextAlignmentJustified:
{
return NSTextAlignmentJustified;
}
case kCTTextAlignmentNatural:
{
return NSTextAlignmentNatural;
}
}
#else
switch (ctTextAlignment)
{
case kCTTextAlignmentLeft:
{
return NSLeftTextAlignment;
}
case kCTTextAlignmentRight:
{
return NSRightTextAlignment;
}
case kCTTextAlignmentCenter:
{
return NSCenterTextAlignment;
}
case kCTTextAlignmentJustified:
{
return NSJustifiedTextAlignment;
}
case kCTTextAlignmentNatural:
{
return NSNaturalTextAlignment;
}
}
#endif
}
#endif
@@ -1,153 +0,0 @@
//
// DTCoreTextGlyphRun.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/25/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE
#import <CoreText/CoreText.h>
#elif TARGET_OS_MAC
#import <ApplicationServices/ApplicationServices.h>
#endif
@class DTCoreTextLayoutLine;
@class DTTextAttachment;
/**
This class is an Objective-C wrapper around `CTRun` and represents a glyph run. That is, a number of characters from the original `NSAttributedString` that share the same characteristics and attributes.
*/
@interface DTCoreTextGlyphRun : NSObject
{
NSRange _stringRange;
}
/**
@name Creating Glyph Runs
*/
/**
Creates a new glyph run from a `CTRun`, belonging to a given layout line and with a given offset from the left line origin.
@param run The Core Text glyph run to wrap
@param layoutLine The layout line that this glyph run belongs to
@param offset The offset from the left line origin to place the glyph run at
@returns An initialized DTCoreTextGlyphRun
*/
- (id)initWithRun:(CTRunRef)run layoutLine:(DTCoreTextLayoutLine *)layoutLine offset:(CGFloat)offset;
/**
@name Drawing
*/
/**
Draws the receiver into the given context with the position that it derives from the layout line it belongs to.
@see drawDecorationInContext: for drawing the receiver's decoration
@param context The graphics context to draw into
*/
- (void)drawInContext:(CGContextRef)context;
/**
Draws the receiver's decoration into the given context with the position that it derives from the layout line it belongs to. Decoration is background highlighting, underline and strike-through.
@param context The graphics context to draw into
*/
- (void)drawDecorationInContext:(CGContextRef)context;
/**
Creates a `CGPath` containing the shapes of all glyphs in the receiver
*/
- (CGPathRef)newPathWithGlyphs;
/**
@name Getting Information
*/
/**
Determines the frame of a specific glyph
@param index The index of the glyph
@return The frame of the glyph
*/
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index;
/**
The bounds of an image encompassing the entire run.
@param context The graphics context used for the measurement
@returns The rectangle containing the result
*/
- (CGRect)imageBoundsInContext:(CGContextRef)context;
/**
The string range (of the attributed string) that is represented by the receiver
@returns The range
*/
- (NSRange)stringRange;
/**
The string indices of the receiver
@returns An array of string indices
*/
- (NSArray *)stringIndices;
/**
The frame rectangle of the glyph run, relative to the layout frame coordinate system
*/
@property (nonatomic, readonly) CGRect frame;
/**
The number of glyphs that the receiver is made up of
*/
@property (nonatomic, readonly) NSInteger numberOfGlyphs;
/**
The Core Text attributes that are shared by all glyphs of the receiver
*/
@property (nonatomic, readonly) NSDictionary *attributes;
/**
Returns `YES` if the receiver is part of a hyperlink, `NO` otherwise
*/
@property (nonatomic, assign, readonly, getter=isHyperlink) BOOL hyperlink;
/**
Returns `YES` if the receiver represents trailing whitespace in a line.
This can be used to avoid drawing of background color, strikeout or underline for empty trailing white space glyph runs.
*/
- (BOOL)isTrailingWhitespace;
/**
The ascent (height above the baseline) of the receiver
*/
@property (nonatomic, readonly) CGFloat ascent;
/**
The descent (height below the baseline) of the receiver
*/
@property (nonatomic, readonly) CGFloat descent;
/**
The leading (additional space above the ascent) of the receiver
*/
@property (nonatomic, readonly) CGFloat leading;
/**
The width of the receiver
*/
@property (nonatomic, readonly) CGFloat width;
/**
`YES` if the writing direction is Right-to-Left, otherwise `NO`
*/
@property (nonatomic, readonly) BOOL writingDirectionIsRightToLeft;
/**
The text attachment of the receiver, or `nil` if there is none
*/
@property (nonatomic, readonly) DTTextAttachment *attachment;
@end
@@ -1,541 +0,0 @@
//
// DTCoreTextGlyphRun.m
// DTCoreText
//
// Created by Oliver Drobnik on 1/25/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTCoreTextGlyphRun.h"
#import "DTCoreTextLayoutLine.h"
#import "DTTextAttachment.h"
#import "DTCoreTextConstants.h"
#import "DTCoreTextParagraphStyle.h"
#import "DTCoreTextFunctions.h"
#import "NSDictionary+DTCoreText.h"
#import <DTFoundation/DTWeakSupport.h>
#import <DTFoundation/DTLog.h>
@implementation DTCoreTextGlyphRun
{
CTRunRef _run;
CGRect _frame;
CGFloat _offset; // x distance from line origin
CGFloat _ascent;
CGFloat _descent;
CGFloat _leading;
CGFloat _width;
BOOL _writingDirectionIsRightToLeft;
BOOL _isTrailingWhitespace;
NSInteger _numberOfGlyphs;
const CGPoint *_glyphPositionPoints;
DT_WEAK_VARIABLE DTCoreTextLayoutLine *_line; // retain cycle, since these objects are retained by the _line
DT_WEAK_VARIABLE NSDictionary *_attributes; // weak because it is owned by _run IVAR
NSArray *_stringIndices;
DTTextAttachment *_attachment;
BOOL _hyperlink;
BOOL _didCheckForAttachmentInAttributes;
BOOL _didCheckForHyperlinkInAttributes;
BOOL _didCalculateMetrics;
BOOL _didDetermineTrailingWhitespace;
}
- (id)initWithRun:(CTRunRef)run layoutLine:(DTCoreTextLayoutLine *)layoutLine offset:(CGFloat)offset
{
self = [super init];
if (self)
{
_run = run;
CFRetain(_run);
_offset = offset;
_line = layoutLine;
}
return self;
}
- (void)dealloc
{
if (_run)
{
CFRelease(_run);
}
}
#ifndef COVERAGE
// exclude method from coverage testing
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ glyphs=%ld %@>", [self class], (long)[self numberOfGlyphs], NSStringFromCGRect(_frame)];
}
#endif
#pragma mark - Drawing
- (void)drawInContext:(CGContextRef)context
{
if (!_run || !context)
{
return;
}
CGAffineTransform textMatrix = CTRunGetTextMatrix(_run);
if (CGAffineTransformIsIdentity(textMatrix))
{
CTRunDraw(_run, context, CFRangeMake(0, 0));
}
else
{
CGPoint pos = CGContextGetTextPosition(context);
// set tx and ty to current text pos according to docs
textMatrix.tx = pos.x;
textMatrix.ty = pos.y;
CGContextSetTextMatrix(context, textMatrix);
CTRunDraw(_run, context, CFRangeMake(0, 0));
// restore identity
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
}
}
- (void)drawDecorationInContext:(CGContextRef)context
{
// get the scaling factor of the current translation matrix
CGAffineTransform ctm = CGContextGetCTM(context);
CGFloat contentScale = MAX(ctm.a, -ctm.d); // needed for rounding operations
if (contentScale<1 || contentScale>2)
{
contentScale = 2;
}
CGFloat smallestPixelWidth = 1.0f/contentScale;
DTColor *backgroundColor = [self.attributes backgroundColor];
// -------------- Line-Out, Underline, Background-Color
BOOL drawStrikeOut = [[_attributes objectForKey:DTStrikeOutAttribute] boolValue];
BOOL drawUnderline = [[_attributes objectForKey:(id)kCTUnderlineStyleAttributeName] boolValue];
if (drawStrikeOut||drawUnderline||backgroundColor)
{
// calculate area covered by non-whitespace
CGRect lineFrame = _line.frame;
// LTR line frames include trailing whitespace in width
// we need to subtract it so that we don't highlight/underline it
if (!_line.writingDirectionIsRightToLeft)
{
lineFrame.size.width -= _line.trailingWhitespaceWidth;
}
// exclude trailing whitespace so that we don't underline too much
CGRect runStrokeBounds = CGRectIntersection(lineFrame, self.frame);
NSInteger superscriptStyle = [[_attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue];
switch (superscriptStyle)
{
case 1:
{
runStrokeBounds.origin.y -= _ascent * 0.47f;
break;
}
case -1:
{
runStrokeBounds.origin.y += _ascent * 0.25f;
break;
}
default:
break;
}
if (backgroundColor)
{
CGRect backgroundColorRect = CGRectIntegral(CGRectMake(runStrokeBounds.origin.x, lineFrame.origin.y, runStrokeBounds.size.width, lineFrame.size.height));
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGContextFillRect(context, backgroundColorRect);
}
if (drawStrikeOut || drawUnderline)
{
BOOL didDrawSomething = NO;
CGContextSaveGState(context);
CTFontRef usedFont = (__bridge CTFontRef)([_attributes objectForKey:(id)kCTFontAttributeName]);
CGFloat fontUnderlineThickness;
if (usedFont)
{
fontUnderlineThickness = CTFontGetUnderlineThickness(usedFont) * smallestPixelWidth;
}
else
{
fontUnderlineThickness = smallestPixelWidth;
}
CGFloat usedUnderlineThickness = DTCeilWithContentScale(fontUnderlineThickness, contentScale);
CGContextSetLineWidth(context, usedUnderlineThickness);
if (drawStrikeOut)
{
CGFloat y;
if (usedFont)
{
CGFloat strokePosition = CTFontGetXHeight(usedFont)/(CGFloat)2.0;
y = DTRoundWithContentScale(runStrokeBounds.origin.y + _ascent - strokePosition, contentScale);
}
else
{
y = DTRoundWithContentScale((runStrokeBounds.origin.y + self.frame.size.height/2.0f + 1), contentScale);
}
if ((int)(usedUnderlineThickness/smallestPixelWidth)%2) // odd line width
{
y += smallestPixelWidth/2.0f; // shift down half a pixel to avoid aliasing
}
CGContextMoveToPoint(context, runStrokeBounds.origin.x, y);
CGContextAddLineToPoint(context, runStrokeBounds.origin.x + runStrokeBounds.size.width, y);
didDrawSomething = YES;
}
// only draw underlines if Core Text didn't draw them yet
if (drawUnderline && !DTCoreTextDrawsUnderlinesWithGlyphs())
{
CGFloat y;
// use lowest underline position of all glyph runs in same line
CGFloat underlinePosition = [_line underlineOffset];
y = DTRoundWithContentScale(_line.baselineOrigin.y + underlinePosition - fontUnderlineThickness/2.0f, contentScale);
if ((int)(usedUnderlineThickness/smallestPixelWidth)%2) // odd line width
{
y += smallestPixelWidth/2.0f; // shift down half a pixel to avoid aliasing
}
CGContextMoveToPoint(context, runStrokeBounds.origin.x, y);
CGContextAddLineToPoint(context, runStrokeBounds.origin.x + runStrokeBounds.size.width, y);
didDrawSomething = YES;
}
if (didDrawSomething)
{
CGContextStrokePath(context);
}
CGContextRestoreGState(context); // restore antialiasing
}
}
}
- (CGPathRef)newPathWithGlyphs
{
CTFontRef font = (__bridge CTFontRef)[self.attributes objectForKey:(id)kCTFontAttributeName];
if (!font)
{
DTLogError(@"CTFont missing on %@", self);
return NULL;
}
const CGGlyph *glyphs = CTRunGetGlyphsPtr(_run);
const CGPoint *positions = CTRunGetPositionsPtr(_run);
CGMutablePathRef mutablePath = CGPathCreateMutable();
for (NSUInteger i = 0; i < CTRunGetGlyphCount(_run); i++)
{
CGGlyph glyph = glyphs[i];
CGPoint position = positions[i];
CGAffineTransform glyphTransform = CTRunGetTextMatrix(_run);
glyphTransform = CGAffineTransformScale(glyphTransform, 1, -1);
CGPathRef glyphPath = CTFontCreatePathForGlyph(font, glyph, &glyphTransform);
CGAffineTransform posTransform = CGAffineTransformMakeTranslation(position.x, position.y);
CGPathAddPath(mutablePath, &posTransform, glyphPath);
CGPathRelease(glyphPath);
}
return mutablePath;
}
#pragma mark - Calculations
- (void)calculateMetrics
{
// calculate metrics
@synchronized(self)
{
if (!_didCalculateMetrics)
{
_width = (CGFloat)CTRunGetTypographicBounds((CTRunRef)_run, CFRangeMake(0, 0), &_ascent, &_descent, &_leading);
_didCalculateMetrics = YES;
}
}
}
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
if (!_glyphPositionPoints)
{
// this is a pointer to the points inside the run, thus no retain necessary
_glyphPositionPoints = CTRunGetPositionsPtr(_run);
}
if (!_glyphPositionPoints || index >= self.numberOfGlyphs)
{
return CGRectNull;
}
CGPoint glyphPosition = _glyphPositionPoints[index];
CGRect rect = CGRectMake(_line.baselineOrigin.x + glyphPosition.x, _line.baselineOrigin.y - _ascent, _offset + _width - glyphPosition.x, _ascent + _descent);
if (index < self.numberOfGlyphs-1)
{
rect.size.width = _glyphPositionPoints[index+1].x - glyphPosition.x;
}
return rect;
}
// TODO: fix indices if the stringRange is modified
- (NSArray *)stringIndices
{
if (!_stringIndices)
{
const CFIndex *indices = CTRunGetStringIndicesPtr(_run);
NSInteger count = self.numberOfGlyphs;
NSMutableArray *array = [NSMutableArray arrayWithCapacity:count];
NSInteger i;
for (i = 0; i < count; i++)
{
[array addObject:[NSNumber numberWithInteger:indices[i]]];
}
_stringIndices = array;
}
return _stringIndices;
}
// bounds of an image encompassing the entire run
- (CGRect)imageBoundsInContext:(CGContextRef)context
{
return CTRunGetImageBounds(_run, context, CFRangeMake(0, 0));
}
// range of the characters from the original string
- (NSRange)stringRange
{
if (!_stringRange.length)
{
CFRange range = CTRunGetStringRange(_run);
_stringRange = NSMakeRange(range.location + _line.stringLocationOffset, range.length);
}
return _stringRange;
}
- (void)fixMetricsFromAttachment
{
if (self.attachment)
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
_descent = 0;
_ascent = self.attachment.displaySize.height;
}
}
- (BOOL)isTrailingWhitespace
{
if (_didDetermineTrailingWhitespace)
{
return _isTrailingWhitespace;
}
BOOL isTrailing;
if (_line.writingDirectionIsRightToLeft)
{
isTrailing = (self == [[_line glyphRuns] objectAtIndex:0]);
}
else
{
isTrailing = (self == [[_line glyphRuns] lastObject]);
}
if (isTrailing)
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
// this is trailing whitespace if it matches the lines's trailing whitespace
if (_line.trailingWhitespaceWidth >= _width)
{
_isTrailingWhitespace = YES;
}
}
_didDetermineTrailingWhitespace = YES;
return _isTrailingWhitespace;
}
#pragma mark Properites
- (NSInteger)numberOfGlyphs
{
if (!_numberOfGlyphs)
{
_numberOfGlyphs = CTRunGetGlyphCount(_run);
}
return _numberOfGlyphs;
}
- (NSDictionary *)attributes
{
if (!_attributes)
{
_attributes = (__bridge NSDictionary *)CTRunGetAttributes(_run);
}
return _attributes;
}
- (DTTextAttachment *)attachment
{
if (!_attachment)
{
if (!_didCheckForAttachmentInAttributes)
{
_attachment = [self.attributes objectForKey:NSAttachmentAttributeName];
_didCheckForAttachmentInAttributes = YES;
}
}
return _attachment;
}
- (BOOL)isHyperlink
{
if (!_hyperlink)
{
if (!_didCheckForHyperlinkInAttributes)
{
_hyperlink = [self.attributes objectForKey:DTLinkAttribute]!=nil;
_didCheckForHyperlinkInAttributes = YES;
}
}
return _hyperlink;
}
- (CGRect)frame
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
return CGRectMake(_line.baselineOrigin.x + _offset, _line.baselineOrigin.y - _ascent, _width, _ascent + _descent);
}
- (CGFloat)width
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
return _width;
}
- (CGFloat)ascent
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
return _ascent;
}
- (CGFloat)descent
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
return _descent;
}
- (CGFloat)leading
{
if (!_didCalculateMetrics)
{
[self calculateMetrics];
}
return _leading;
}
- (BOOL)writingDirectionIsRightToLeft
{
CTRunStatus status = CTRunGetStatus(_run);
return (status & kCTRunStatusRightToLeft)!=0;
}
@synthesize frame = _frame;
@synthesize numberOfGlyphs = _numberOfGlyphs;
@synthesize attributes = _attributes;
@synthesize ascent = _ascent;
@synthesize descent = _descent;
@synthesize leading = _leading;
@synthesize attachment = _attachment;
@synthesize writingDirectionIsRightToLeft = _writingDirectionIsRightToLeft;
@end
@@ -1,33 +0,0 @@
//
// DTCoreTextLayoutFrame+Cursor.h
// DTCoreText
//
// Created by Oliver Drobnik on 10.07.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCoreTextLayoutFrame.h"
/**
The **Cursor** category extends DTCoreTextLayoutFrame for working with a caret and determine the string index of touch coordinates.
*/
@interface DTCoreTextLayoutFrame (Cursor)
/**
Determines the closest string index to a point in the receiver's frame.
This can be used to find the cursor position to position an input caret at.
@param point The point
@returns The resulting string index
*/
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point;
/**
The rectangle to draw a caret for a given index
@param index The string index for which to determine a cursor frame
@returns The cursor rectangle
*/
- (CGRect)cursorRectAtIndex:(NSInteger)index;
@end
@@ -1,114 +0,0 @@
//
// DTCoreTextLayoutFrame+Cursor.m
// DTCoreText
//
// Created by Oliver Drobnik on 10.07.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCoreTextLayoutFrame+Cursor.h"
#import "DTCoreTextLayoutLine.h"
@implementation DTCoreTextLayoutFrame (Cursor)
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point
{
NSArray *lines = self.lines;
if (![lines count])
{
return NSNotFound;
}
DTCoreTextLayoutLine *firstLine = [lines objectAtIndex:0];
if (point.y < CGRectGetMinY(firstLine.frame))
{
return 0;
}
DTCoreTextLayoutLine *lastLine = [lines lastObject];
if (point.y > CGRectGetMaxY(lastLine.frame))
{
NSRange stringRange = [self visibleStringRange];
if (stringRange.length)
{
return NSMaxRange([self visibleStringRange])-1;
}
}
// find closest line
DTCoreTextLayoutLine *closestLine = nil;
CGFloat closestDistance = CGFLOAT_MAX;
for (DTCoreTextLayoutLine *oneLine in lines)
{
// line contains point
if (CGRectGetMinY(oneLine.frame) <= point.y && CGRectGetMaxY(oneLine.frame) >= point.y)
{
closestLine = oneLine;
break;
}
CGFloat top = CGRectGetMinY(oneLine.frame);
CGFloat bottom = CGRectGetMaxY(oneLine.frame);
CGFloat distance = CGFLOAT_MAX;
if (top > point.y)
{
distance = top - point.y;
}
else if (bottom < point.y)
{
distance = point.y - bottom;
}
if (distance < closestDistance)
{
closestLine = oneLine;
closestDistance = distance;
}
}
if (!closestLine)
{
return NSNotFound;
}
NSInteger closestIndex = [closestLine stringIndexForPosition:point];
NSInteger maxIndex = NSMaxRange([closestLine stringRange])-1;
if (closestIndex > maxIndex)
{
closestIndex = maxIndex;
}
if (closestIndex>=0)
{
return closestIndex;
}
return NSNotFound;
}
- (CGRect)cursorRectAtIndex:(NSInteger)index
{
DTCoreTextLayoutLine *line = [self lineContainingIndex:index];
if (!line)
{
return CGRectZero;
}
CGFloat offset = [line offsetForStringIndex:index];
CGRect rect = line.frame;
rect.size.width = 3.0;
rect.origin.x += offset;
return rect;
}
@end
@@ -1,385 +0,0 @@
//
// DTCoreTextLayoutFrame.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/24/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTCoreTextConstants.h"
#import <CoreText/CoreText.h>
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <UIKit/UIKit.h>
#endif
#if TARGET_OS_OSX
#import <AppKit/AppKit.h>
#import <ApplicationServices/ApplicationServices.h>
#endif
@class DTCoreTextLayoutLine;
@class DTTextBlock;
/**
A handler block that is called whenever a text block attributed is encountered during text drawing
*/
typedef void (^DTCoreTextLayoutFrameTextBlockHandler)(DTTextBlock *textBlock, CGRect frame, CGContextRef context, BOOL *shouldDrawDefaultBackground);
/**
The drawing options for DTCoreTextLayoutFrame
*/
typedef NS_ENUM(NSUInteger, DTCoreTextLayoutFrameDrawingOptions)
{
/**
The default method for drawing draws links and attachments. Links are drawn non-highlighted
*/
DTCoreTextLayoutFrameDrawingDefault = 1<<0,
/**
Links are not drawn, e.g. if they are displayed via custom buttons
*/
DTCoreTextLayoutFrameDrawingOmitLinks = 1<<1,
/**
Text attachments are omitted from drawing, e.g. if they are displayed via custom views
*/
DTCoreTextLayoutFrameDrawingOmitAttachments = 1<<2,
/**
If links are drawn they are displayed with the highlighted variant
*/
DTCoreTextLayoutFrameDrawingDrawLinksHighlighted = 1<<3
} ;
@class DTCoreTextLayouter;
/**
This class represents a single frame of text and basically wraps CTFrame. It provides an array of text lines that fit in the given rectangle.
Both styles of layouting are supported: open ended (suitable for scroll views) and limited to a given rectangle. To use the open-ended style specify `CGFLOAT_HEIGHT_UNKNOWN` for the <frame> height when creating a layout frame.
The array of lines is built lazily the first time it is accessed or - for open-ended frames - when the frame property is being queried.
*/
@interface DTCoreTextLayoutFrame : NSObject
{
CGRect _frame;
NSArray *_lines;
NSArray *_paragraphRanges;
NSArray *_textAttachments;
NSAttributedString *_attributedStringFragment;
}
/**
@name Creating Layout Frames
*/
/**
Creates a Layout Frame with the given frame using the attributed string loaded into the layouter.
@param frame The rectangle specifying origin and size of available for text. Specify `CGFLOAT_WIDTH_UNKNOWN` to not limit the width. Specify `CGFLOAT_HEIGHT_UNKNOWN` to not limit the height.
@param layouter A reference to the layouter for this text box.
*/
- (id)initWithFrame:(CGRect)frame layouter:(DTCoreTextLayouter *)layouter;
/**
Creates a Layout Frame with the given frame using the attributed string loaded into the layouter.
@param frame The rectangle specifying origin and size of available for text. Specify `CGFLOAT_WIDTH_UNKNOWN` to not limit the width. Specify `CGFLOAT_HEIGHT_UNKNOWN` to not limit the height.
@param layouter A reference to the layouter for the receiver. Note: The layouter owns the attributed string.
@param range The range within the attributed string to layout into the receiver.
*/
- (id)initWithFrame:(CGRect)frame layouter:(DTCoreTextLayouter *)layouter range:(NSRange)range;
/**
@name Getting Information
*/
/**
The string range that is visible i.e. fits into the given rectangle. For open-ended frames this is typically the entire string. For frame-contrained layout frames it is the substring that fits.
*/
- (NSRange)visibleStringRange;
/**
This is a copy of the attributed string owned by the layouter of the receiver.
*/
- (NSAttributedString *)attributedStringFragment;
/**
An array that maps glyphs with string indices.
*/
- (NSArray *)stringIndices;
/**
The frame rectangle for the layout frame.
*/
@property (nonatomic, assign, readonly) CGRect frame;
/**
Calculates the frame that is covered by the text content.
The result is calculated by enumerating over all lines and creating a union over all their frames. This is different than the frame property since this gets calculated.
@returns The area that is covered by the text content.
@note The width depends on how many glyphs Core Text was able to fit into a line. A line that gets broken might not have glyphs all the way to the margin. The y origin is always adjusted to be the same as frame since the first line might have some leading. The height is the minimum height that fits all layout lines.
*/
- (CGRect)intrinsicContentFrame;
/**
@name Drawing
*/
/**
Draws the receiver into the given graphics context.
@warning This method is deprecated, use -[DTCoreTextLayoutFrame drawInContext:options:] instead
@param context A graphics context to draw into
@param drawImages Whether images should be drawn together with the text. If you specify `NO` then space is left blank where images would go and you have to add your own views to display these images.
@param drawLinks Whether hyperlinks should be drawn together with the text. If you specify `NO` then space is left blank where links would go and you have to add your own views to display these images.
*/
- (void)drawInContext:(CGContextRef)context drawImages:(BOOL)drawImages drawLinks:(BOOL)drawLinks __attribute__((deprecated("use -[DTCoreTextLayoutFrame drawInContext:options:] instead")));
/**
Draws the receiver into the given graphics context.
@param context A graphics context to draw into
@param options The drawing options. See DTCoreTextLayoutFrameDrawingOptions for available options.
*/
- (void)drawInContext:(CGContextRef)context options:(DTCoreTextLayoutFrameDrawingOptions)options;
/**
Set a custom handler to be executed before text belonging to a text block is drawn. Of type <DTCoreTextLayoutFrameTextBlockHandler>.
*/
@property (nonatomic, copy) DTCoreTextLayoutFrameTextBlockHandler textBlockHandler;
/**
@name Working with Glyphs
*/
/**
Retrieves the index of the text line that contains the given glyph index.
@param index The index of the glyph
@returns The index of the line containing this glyph
*/
- (NSInteger)lineIndexForGlyphIndex:(NSInteger)index;
/**
Retrieves the frame of the glyph at the given glyph index.
@param index The index of the glyph
@returns The frame of this glyph
*/
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index;
/**
@name Working with Text Lines
*/
/**
The text lines that belong to the receiver.
*/
@property (nonatomic, strong, readonly) NSArray *lines;
/**
The text lines that are visible inside the given rectangle. Also incomplete lines are included.
@param rect The rectangle
@returns An array, sorted from top to bottom, of lines at least partially visible
*/
- (NSArray *)linesVisibleInRect:(CGRect)rect;
/**
The text lines that are visible inside the given rectangle. Only fully visible lines are included.
@param rect The rectangle
@returns An array, sorted from top to bottom, of lines fully visible
*/
- (NSArray *)linesContainedInRect:(CGRect)rect;
/**
The layout line that contains the given string index.
@param index The string index
@returns The layout line that this index belongs to
*/
- (DTCoreTextLayoutLine *)lineContainingIndex:(NSUInteger)index;
/**
Determins if the given line is the first in a paragraph.
This is needed for example to determine whether paragraphSpaceBefore needs to be applied before it.
@param line The Line
@returns `YES` if the given line is the first in a paragraph
*/
- (BOOL)isLineFirstInParagraph:(DTCoreTextLayoutLine *)line;
/**
Determins if the given line is the last in a paragraph.
This is needed for example to determine whether paragraph spacing needs to be applied after it.
@param line The Line
@returns `YES` if the given line is the last in a paragraph
*/
- (BOOL)isLineLastInParagraph:(DTCoreTextLayoutLine *)line;
/**
Finds the appropriate baseline origin for a line to position it at the correct distance from a previous line.
Support Layout options are:
- DTCoreTextLayoutFrameLinePositioningAlgorithmWebKit,
- DTCoreTextLayoutFrameLinePositioningAlgorithmLegacy
@param line The line
@param previousLine The line after which to position the line.
@param options The layout options to employ for positioning lines
@returns The correct baseline origin for the line.
*/
- (CGPoint)baselineOriginToPositionLine:(DTCoreTextLayoutLine *)line afterLine:(DTCoreTextLayoutLine *)previousLine options:(DTCoreTextLayoutFrameLinePositioningOptions)options;
/**
Finds the appropriate baseline origin for a line to position it at the correct distance from a previous line using the DTCoreTextLayoutFrameLinePositioningOptionAlgorithmLegacy algorithm.
@warning This method is deprecated, use -[baselineOriginToPositionLine:afterLine:algorithm:] instead
@param line The line
@param previousLine The line after which to position the line.
@returns The correct baseline origin for the line.
*/
- (CGPoint)baselineOriginToPositionLine:(DTCoreTextLayoutLine *)line afterLine:(DTCoreTextLayoutLine *)previousLine __attribute__((deprecated("use use -[baselineOriginToPositionLine:afterLine:algorithm:] instead")));;
/**
The ratio to decide when to create a justified line
*/
@property (nonatomic, readwrite) CGFloat justifyRatio;
/**
@name Text Attachments
*/
/**
The array of all <DTTextAttachment> instances that belong to the receiver.
@returns All text attachments of the receiver.
*/
- (NSArray *)textAttachments;
/**
The array of all DTTextAttachment instances that belong to the receiver which also match the specified predicate.
@param predicate A predicate that uses properties of <DTTextAttachment> to reduce the returned array
@returns A filtered array of text attachments.
*/
- (NSArray *)textAttachmentsWithPredicate:(NSPredicate *)predicate;
/**
@name Getting Paragraph Info
*/
/**
Finding which paragraph a given string index belongs to.
@param stringIndex The index in the string to look for
@returns The index of the paragraph, numbered from 0
*/
- (NSUInteger)paragraphIndexContainingStringIndex:(NSUInteger)stringIndex;
/**
Determines the paragraph range (of paragraph indexes) that encompass the entire given string Range.
@param stringRange The string range for which the paragraph range is sought for
@returns The range of paragraphs that fully enclose the string range
*/
- (NSRange)paragraphRangeContainingStringRange:(NSRange)stringRange;
/**
The text lines that belong to the specified paragraph.
@param index The index of the paragraph
@returns An array, sorted from top to bottom, of lines in this paragraph
*/
- (NSArray *)linesInParagraphAtIndex:(NSUInteger)index;
/**
An array of `NSRange` values encapsulated in `NSValue` instances. Each range is the string range contained in the corresponding paragraph.
*/
@property (nonatomic, strong, readonly) NSArray *paragraphRanges;
/**
@name Debugging
*/
/**
Switches on the debug drawing mode where individual glyph runs, baselines, et cetera get individually marked.
@param debugFrames if the debug drawing should occur
*/
+ (void)setShouldDrawDebugFrames:(BOOL)debugFrames;
/**
@returns the current value of the debug frame drawing
*/
+ (BOOL)shouldDrawDebugFrames;
/**
@name Truncation
*/
/**
Maximum number of lines to display before truncation. Default is 0 which indicates no limit.
*/
@property(nonatomic, assign) NSInteger numberOfLines;
/**
Line break mode used to indicate how truncation should occur
*/
@property(nonatomic, assign) NSLineBreakMode lineBreakMode;
/**
Optional attributed string to use as truncation indicator. If nil, will use "…" w/ attributes taken from text being truncated
*/
@property(nonatomic, strong)NSAttributedString *truncationString;
@end
File diff suppressed because it is too large Load Diff
@@ -1,39 +0,0 @@
//
// DTCoreTextLayoutFrameAccessibilityElementGenerator.h
// DTCoreText
//
// Created by Austen Green on 3/13/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTAccessibilityElement.h"
@class DTCoreTextLayoutFrame, DTTextAttachment;
/**
A block that provides accessibility information for the passed text attachments
*/
typedef id(^DTAttachmentViewProvider)(DTTextAttachment *textAttachment);
/**
Generates an array of objects conforming to the UIAccessibility informal protocol based on a <DTCoreTextLayoutFrame>.
*/
@interface DTCoreTextLayoutFrameAccessibilityElementGenerator : NSObject
/**
The designated initializer. The DTAttachmentViewProvider block may be used to provide custom subviews in place of a static accessibility element.
@param frame The <DTCoreTextLayoutFrame> to generate accessibility elements for.
@param view The logical superview of the elements - the view that owns the local coordinate system for drawing the frame.
@param block A callback block which takes a <DTTextAttachment> object and returns an object that conforms to the UIAccessibility informal protocol.
@returns Returns an array of objects conforming to the UIAccessibility informal protocol, suitable for presentation for the VoiceOver system.
*/
- (NSArray *)accessibilityElementsForLayoutFrame:(DTCoreTextLayoutFrame *)frame view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block;
@end
#endif
@@ -1,124 +0,0 @@
//
// DTCoreTextLayoutFrameAccessibilityElementGenerator.m
// DTCoreText
//
// Created by Austen Green on 3/13/13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCoreTextLayoutFrameAccessibilityElementGenerator.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTCoreTextLayoutFrame.h"
#import "DTCoreTextLayoutLine.h"
#import "DTCoreTextGlyphRun.h"
#import "DTAccessibilityElement.h"
#import "DTCoreTextConstants.h"
#import "DTTextAttachment.h"
@implementation DTCoreTextLayoutFrameAccessibilityElementGenerator
- (NSArray *)accessibilityElementsForLayoutFrame:(DTCoreTextLayoutFrame *)frame view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block
{
NSMutableArray *elements = [NSMutableArray array];
for (NSUInteger idx = 0; idx < frame.paragraphRanges.count; idx++)
{
NSArray *paragraphElements = [self accessibilityElementsInParagraphAtIndex:idx layoutFrame:frame view:view attachmentViewProvider:block];
[elements addObjectsFromArray:paragraphElements];
}
return elements;
}
- (NSArray *)accessibilityElementsInParagraphAtIndex:(NSUInteger)index layoutFrame:(DTCoreTextLayoutFrame *)frame view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block
{
NSMutableArray *elements = [NSMutableArray array];
[self enumerateAccessibleGroupsInFrame:frame forParagraphAtIndex:index usingBlock:^(NSDictionary *attrs, NSRange substringRange, BOOL *stop, NSArray *runs) {
id element = [self accessibilityElementForTextInAttributedString:frame.attributedStringFragment atRange:substringRange attributes:attrs run:runs view:view attachmentViewProvider:block];
if (element)
[elements addObject:element];
}];
return elements;
}
- (void)enumerateAccessibleGroupsInFrame:(DTCoreTextLayoutFrame *)frame forParagraphAtIndex:(NSUInteger)index usingBlock:(void(^)(NSDictionary *attrs, NSRange substringRange, BOOL *stop, NSArray *runs))block
{
NSValue *value = [frame.paragraphRanges objectAtIndex:index];
NSRange paragraphRange = value.rangeValue;
NSArray *lines = [frame linesInParagraphAtIndex:index];
[frame.attributedStringFragment enumerateAttributesInRange:paragraphRange options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
NSMutableArray *runs = [NSMutableArray array];
for (DTCoreTextLayoutLine *line in lines)
{
[runs addObjectsFromArray:[line glyphRunsWithRange:range]];
}
block(attrs, range, stop, runs);
}];
}
- (id)accessibilityElementForTextInAttributedString:(NSAttributedString *)attributedString atRange:(NSRange)range attributes:(NSDictionary *)attributes run:(NSArray *)runs view:(UIView *)view attachmentViewProvider:(DTAttachmentViewProvider)block
{
DTTextAttachment *attachment = [attributes objectForKey:NSAttachmentAttributeName];
if (attachment != nil)
return [self viewForAttachment:attachment attachmentViewProvider:block];
else
return [self accessibilityElementForTextInAttributedString:attributedString atRange:range attributes:attributes run:runs view:view];
}
- (DTAccessibilityElement *)accessibilityElementForTextInAttributedString:(NSAttributedString *)attributedString atRange:(NSRange)range attributes:(NSDictionary *)attributes run:(NSArray *)runs view:(UIView *)view
{
NSString *text = [attributedString.string substringWithRange:range];
DTAccessibilityElement *element = [[DTAccessibilityElement alloc] initWithParentView:view];
element.accessibilityLabel = text;
element.localCoordinateAccessibilityFrame = [self frameForRuns:runs];
// We're trying to keep the accessibility frame behavior consistent with web view, which seems to do a union of the rects for all the runs composing a single accessibility group,
// even if that spans across multiple lines. Set the local coordinate activation point to support multi-line links. A link that is at the end of one line and
// wraps to the beginning of the next would have a rect that's the size of both lines combined. The center of that rect would be outside the hit areas for either of the
// runs individually, so we set the accessibility activation point to be the origin of the first run.
if (runs.count > 1)
{
DTCoreTextGlyphRun *run = [runs objectAtIndex:0];
element.localCoordinateAccessibilityActivationPoint = run.frame.origin;
}
element.accessibilityTraits = UIAccessibilityTraitStaticText;
if ([attributes objectForKey:DTLinkAttribute])
element.accessibilityTraits |= UIAccessibilityTraitLink;
return element;
}
- (UIView *)viewForAttachment:(DTTextAttachment *)attachment attachmentViewProvider:(DTAttachmentViewProvider)block
{
UIView *view = nil;
if (block)
{
view = block(attachment);
}
return view;
}
- (CGRect)frameForRuns:(NSArray *)runs
{
CGRect frame = CGRectNull;
for (DTCoreTextGlyphRun *run in runs)
frame = CGRectUnion(frame, run.frame);
return frame;
}
@end
#endif
@@ -1,220 +0,0 @@
//
// DTCoreTextLayoutLine.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/24/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE
#import <CoreText/CoreText.h>
#elif TARGET_OS_MAC
#import <ApplicationServices/ApplicationServices.h>
#endif
@class DTCoreTextLayoutFrame;
@class DTCoreTextParagraphStyle;
@class DTTextBlock;
/**
This class represents one layouted line and contains a number of glyph runs.
*/
@interface DTCoreTextLayoutLine : NSObject
{
// IVAR required by DTRichTextEditor, used in category
NSInteger _stringLocationOffset; // offset to modify internal string location to get actual location
}
/**
@name Creating Layout Lines
*/
/**
Creates a layout line from a given `CTLine`
@param line The Core Text line to wrap
@returns A prepared layout line
*/
- (id)initWithLine:(CTLineRef)line;
/**
Creates a layout line from a given `CTLine`
@param line The Core Text line to wrap
@param stringLocationOffset Offset to modify internal string location to get actual location
@returns A prepared layout line
*/
- (id)initWithLine:(CTLineRef)line stringLocationOffset:(NSInteger)stringLocationOffset;
/**
@name Drawing Layout Lines
*/
/**
Draws the receiver in a given graphics context
@param context The graphics context to draw into
*/
- (void)drawInContext:(CGContextRef)context;
/**
Creates a `CGPath` containing the shapes of all glyphs in the line
*/
- (CGPathRef)newPathWithGlyphs;
/**
@name Getting Information about Layout Lines
*/
/**
The range in the original string that is represented by the receiver
@returns The string strange
*/
- (NSRange)stringRange;
/**
The number of glyphs the receiver consists of
@returns the number of glyphs
*/
- (NSInteger)numberOfGlyphs;
/**
Determines the frame of a specific glyph
@param index The index of the glyph
@return The frame of the glyph
*/
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index;
/**
Retrieves the glyphRuns with a given range
@param range The range
@returns An array of glyph runs
*/
- (NSArray *)glyphRunsWithRange:(NSRange)range;
/**
The frame of a number of glyphs with a given range
@param range The range
@returns The rectangle containing the result
*/
- (CGRect)frameOfGlyphsWithRange:(NSRange)range;
/**
The bounds of an image encompassing the entire run.
@param context The graphics context used for the measurement
@returns The rectangle containing the result
*/
- (CGRect)imageBoundsInContext:(CGContextRef)context;
/**
The string indices of the receiver
@returns An array of string indices
*/
- (NSArray *)stringIndices;
/**
Determines the graphical offset for a given string index
@param index The string index
@returns The offset
*/
- (CGFloat)offsetForStringIndex:(NSInteger)index;
/**
Determines the string index that is closest to a given point
@param position The position to determine the string index for
@returns The string index
*/
- (NSInteger)stringIndexForPosition:(CGPoint)position;
/**
The frame of the receiver relative to the layout frame
*/
@property (nonatomic, assign) CGRect frame;
/**
The glyph runs that the line contains.
*/
@property (nonatomic, readonly) NSArray *glyphRuns;
/**
The ascent (height above the baseline) of the receiver
*/
@property (nonatomic, assign) CGFloat ascent; // needs to be modifiable
/**
The descent (height below the baseline) of the receiver
*/
@property (nonatomic, readonly) CGFloat descent;
/**
The leading (additional space above the ascent) of the receiver
*/
@property (nonatomic, readonly) CGFloat leading;
/**
The width of the trailing whitespace of the receiver
*/
@property (nonatomic, readonly) CGFloat trailingWhitespaceWidth;
/**
The offset for the underline in positive points measured from the baseline. This is the maximum underline value of the fonts of all glyph runs of the receiver.
*/
@property (nonatomic, readonly) CGFloat underlineOffset;
/**
The line height of the line. This is determined by getting the maximum font size of all glyph runs of the receiver.
*/
@property (nonatomic, readonly) CGFloat lineHeight;
/**
The paragraph style of the paragraph this line belongs to. All lines in a paragraph are supposed to have the same paragraph style, so this takes the paragraph style of the first glyph run
*/
@property (nonatomic, readonly) DTCoreTextParagraphStyle *paragraphStyle;
/**
The text blocks that the receiver belongs to.
*/
@property (nonatomic, readonly) NSArray *textBlocks;
/**
The text attachments occurring in glyph runs of the receiver.
*/
@property (nonatomic, readonly) NSArray *attachments;
/**
The baseline origin of the receiver
*/
@property (nonatomic, assign) CGPoint baselineOrigin;
/**
`YES` if the writing direction is Right-to-Left, otherwise `NO`
*/
@property (nonatomic, assign) BOOL writingDirectionIsRightToLeft;
/**
The offset to modify internal string location to get actual location
*/
@property (nonatomic, readonly) NSInteger stringLocationOffset;
/**
Method to efficiently determine if the receiver is a horizontal rule.
Note: This is used to shortcut drawing of text lines and to allow a horizontal rule line have an "endlessly wide" width so that it gets picked up by [DTCoreTextLayoutFrame linesVisibleInRect:].
*/
- (BOOL)isHorizontalRule;
/**
@name Creating Variants
*/
/**
Creates a version of the receiver that is justified to the given width.
@param justificationFactor Full or partial justification. When set to `1.0` or greater, full justification is performed. If this parameter is set to less than `1.0`, varying degrees of partial justification are performed. If it is set to `0` or less, no justification is performed.
@param justificationWidth The width to which the resultant line is justified. If justificationWidth is less than the actual width of the line, then negative justification is performed (that is, glyphs are squeezed together).
*/
- (DTCoreTextLayoutLine *)justifiedLineWithFactor:(CGFloat)justificationFactor justificationWidth:(CGFloat)justificationWidth;
@end
@@ -1,571 +0,0 @@
//
// DTCoreTextLayoutLine.m
// DTCoreText
//
// Created by Oliver Drobnik on 1/24/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTCoreTextLayoutLine.h"
#import "DTCoreTextGlyphRun.h"
#import "DTCoreTextLayoutFrame.h"
#import "DTCoreTextLayouter.h"
#import "DTTextAttachment.h"
#import "NSDictionary+DTCoreText.h"
#import "DTTextBlock.h"
#import "DTCoreTextConstants.h"
#import "DTCoreTextFunctions.h"
@interface DTCoreTextLayoutLine ()
@property (nonatomic, strong) NSArray *glyphRuns;
@end
@implementation DTCoreTextLayoutLine
{
CGRect _frame;
CTLineRef _line;
CGPoint _baselineOrigin;
CGFloat _ascent;
CGFloat _descent;
CGFloat _leading;
CGFloat _width;
CGFloat _trailingWhitespaceWidth;
CGFloat _underlineOffset;
CGFloat _lineHeight;
NSArray *_glyphRuns;
BOOL _didCalculateMetrics;
BOOL _writingDirectionIsRightToLeft;
BOOL _needsToDetectWritingDirection;
BOOL _hasScannedGlyphRunsForValues;
}
- (id)initWithLine:(CTLineRef)line
{
return [self initWithLine:line stringLocationOffset:0];
}
- (id)initWithLine:(CTLineRef)line stringLocationOffset:(NSInteger)stringLocationOffset
{
if (!line)
{
return nil;
}
if ((self = [super init]))
{
_line = line;
CFRetain(_line);
// writing direction
_needsToDetectWritingDirection = YES;
_stringLocationOffset = stringLocationOffset;
}
return self;
}
- (void)dealloc
{
CFRelease(_line);
}
#ifndef COVERAGE
// exclude method from coverage testing
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ origin=%@ frame=%@ range=%@", [self class], NSStringFromCGPoint(_baselineOrigin), NSStringFromCGRect(self.frame), NSStringFromRange([self stringRange])];
}
#endif
- (NSRange)stringRange
{
CFRange range = CTLineGetStringRange(_line);
// add offset if there is one, i.e. from merged lines
range.location += _stringLocationOffset;
return NSMakeRange(range.location, range.length);
}
- (NSInteger)numberOfGlyphs
{
NSInteger ret = 0;
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
{
ret += [oneRun numberOfGlyphs];
}
return ret;
}
#pragma mark - Drawing
- (void)drawInContext:(CGContextRef)context
{
CTLineDraw(_line, context);
}
- (CGPathRef)newPathWithGlyphs
{
// mutable path for the line
CGMutablePathRef mutablePath = CGPathCreateMutable();
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
{
CGPathRef glyphPath = [oneRun newPathWithGlyphs];
CGAffineTransform posTransform = CGAffineTransformMakeTranslation(_baselineOrigin.x, _baselineOrigin.y);
CGPathAddPath(mutablePath, &posTransform, glyphPath);
CGPathRelease(glyphPath);
}
return mutablePath;
}
#pragma mark - Creating Variants
- (DTCoreTextLayoutLine *)justifiedLineWithFactor:(CGFloat)justificationFactor justificationWidth:(CGFloat)justificationWidth
{
// make this line justified
CTLineRef justifiedLine = CTLineCreateJustifiedLine(_line, justificationFactor, justificationWidth);
DTCoreTextLayoutLine *newLine = [[DTCoreTextLayoutLine alloc] initWithLine:justifiedLine];
CFRelease(justifiedLine);
return newLine;
}
#pragma mark - Calculations
- (NSArray *)stringIndices
{
NSMutableArray *array = [NSMutableArray array];
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
{
[array addObjectsFromArray:[oneRun stringIndices]];
}
return array;
}
- (CGRect)frameOfGlyphAtIndex:(NSInteger)index
{
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
{
NSInteger count = [oneRun numberOfGlyphs];
if (index >= count)
{
index -= count;
}
else
{
return [oneRun frameOfGlyphAtIndex:index];
}
}
return CGRectZero;
}
- (NSArray *)glyphRunsWithRange:(NSRange)range
{
NSMutableArray *tmpArray = [NSMutableArray arrayWithCapacity:[self numberOfGlyphs]];
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
{
NSRange runRange = [oneRun stringRange];
// intersect these ranges
NSRange intersectionRange = NSIntersectionRange(range, runRange);
// if intersection is longer than zero length they intersect
if (intersectionRange.length)
{
[tmpArray addObject:oneRun];
}
}
return tmpArray;
}
- (CGRect)frameOfGlyphsWithRange:(NSRange)range
{
NSArray *glyphRuns = [self glyphRunsWithRange:range];
CGRect tmpRect = CGRectMake(CGFLOAT_MAX, CGFLOAT_MAX, 0, 0);
for (DTCoreTextGlyphRun *oneRun in glyphRuns)
{
CGRect glyphFrame = oneRun.frame;
if (glyphFrame.origin.x < tmpRect.origin.x)
{
tmpRect.origin.x = glyphFrame.origin.x;
}
if (glyphFrame.origin.y < tmpRect.origin.y)
{
tmpRect.origin.y = glyphFrame.origin.y;
}
if (glyphFrame.size.height > tmpRect.size.height)
{
tmpRect.size.height = glyphFrame.size.height;
}
tmpRect.size.width = glyphFrame.origin.x + glyphFrame.size.width - tmpRect.origin.x;
}
CGFloat maxX = CGRectGetMaxX(self.frame) - _trailingWhitespaceWidth;
if (CGRectGetMaxX(tmpRect) > maxX)
{
tmpRect.size.width = maxX - tmpRect.origin.x;
}
return tmpRect;
}
// bounds of an image encompassing the entire run
- (CGRect)imageBoundsInContext:(CGContextRef)context
{
return CTLineGetImageBounds(_line, context);
}
- (CGFloat)offsetForStringIndex:(NSInteger)index
{
// subtract offset if there is one, i.e. from merged lines
index -= _stringLocationOffset;
return CTLineGetOffsetForStringIndex(_line, index, NULL);
}
- (NSInteger)stringIndexForPosition:(CGPoint)position
{
// position is in same coordinate system as frame
CGPoint adjustedPosition = position;
CGRect frame = self.frame;
adjustedPosition.x -= frame.origin.x;
adjustedPosition.y -= frame.origin.y;
NSInteger index = CTLineGetStringIndexForPosition(_line, adjustedPosition);
// add offset if there is one, i.e. from merged lines
index += _stringLocationOffset;
return index;
}
- (void)_calculateMetrics
{
@synchronized(self)
{
if (!_didCalculateMetrics)
{
_width = (CGFloat)CTLineGetTypographicBounds(_line, &_ascent, &_descent, &_leading);
_trailingWhitespaceWidth = (CGFloat)CTLineGetTrailingWhitespaceWidth(_line);
_didCalculateMetrics = YES;
}
}
}
- (BOOL)isHorizontalRule
{
// HR is only a single \n
if (self.stringRange.length>1)
{
return NO;
}
NSArray *runs = self.glyphRuns;
// thus only a single glyphRun
if ([runs count]>1)
{
return NO;
}
DTCoreTextGlyphRun *singleRun = [runs lastObject];
if ([singleRun.attributes objectForKey:DTHorizontalRuleStyleAttribute])
{
return YES;
}
return NO;
}
#pragma mark Determining Values from the glyph runs
- (void)_scanGlyphRunsForValues
{
@synchronized(self)
{
CGFloat maxOffset = 0;
CGFloat maxFontSize = 0;
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
{
CTFontRef usedFont = (__bridge CTFontRef)([oneRun.attributes objectForKey:(id)kCTFontAttributeName]);
if (usedFont)
{
maxOffset = MAX(maxOffset, fabs(CTFontGetUnderlinePosition(usedFont)));
maxFontSize = MAX(maxFontSize, CTFontGetSize(usedFont));
}
}
_underlineOffset = maxOffset;
_lineHeight = maxFontSize;
_hasScannedGlyphRunsForValues= YES;
}
}
#pragma mark - Properties
- (NSArray *)glyphRuns
{
@synchronized(self)
{
if (!_glyphRuns)
{
// run array is owned by line
CFArrayRef runs = CTLineGetGlyphRuns(_line);
CFIndex runCount = CFArrayGetCount(runs);
if (runCount)
{
NSMutableArray *tmpArray = [[NSMutableArray alloc] initWithCapacity:runCount];
for (CFIndex i=0; i<runCount; i++)
{
CTRunRef oneRun = CFArrayGetValueAtIndex(runs, i);
CGPoint *positions = (CGPoint*)CTRunGetPositionsPtr(oneRun);
BOOL shouldFreePositions = NO;
if (positions == NULL) // Ptr gave NULL, we'll need to copy positions array and later free it
{
CFIndex glyphCount = CTRunGetGlyphCount(oneRun);
shouldFreePositions = YES;
size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;
CGPoint *positionsBuffer = malloc(positionsBufferSize);
CTRunGetPositions(oneRun, CFRangeMake(0, 0), positionsBuffer);
positions = positionsBuffer;
}
// assumption: position of first glyph is also the correct offset of the entire run
CGPoint position = positions[0];
DTCoreTextGlyphRun *glyphRun = [[DTCoreTextGlyphRun alloc] initWithRun:oneRun layoutLine:self offset:position.x];
[tmpArray addObject:glyphRun];
if ( shouldFreePositions )
{
free(positions);
}
}
_glyphRuns = tmpArray;
}
}
return _glyphRuns;
}
}
- (CGRect)frame
{
if (!_didCalculateMetrics)
{
[self _calculateMetrics];
}
CGRect frame = CGRectMake(_baselineOrigin.x, _baselineOrigin.y - _ascent, _width, _ascent + _descent);
// make sure that HR are extremely wide to be be picked up
if ([self isHorizontalRule])
{
frame.size.width = CGFLOAT_MAX;
}
return frame;
}
- (CGFloat)width
{
if (!_didCalculateMetrics)
{
[self _calculateMetrics];
}
return _width;
}
- (NSArray *)attachments
{
NSMutableArray *tmpArray = [NSMutableArray array];
for (DTCoreTextGlyphRun *oneRun in self.glyphRuns)
{
DTTextAttachment *attachment = oneRun.attachment;
if (attachment)
{
[tmpArray addObject:attachment];
}
}
if ([tmpArray count])
{
return tmpArray;
}
return nil;
}
- (CGFloat)ascent
{
if (!_didCalculateMetrics)
{
[self _calculateMetrics];
}
return _ascent;
}
- (void)setAscent:(CGFloat)ascent
{
// need to get metrics because otherwise ascent gets overwritten
if (!_didCalculateMetrics)
{
[self _calculateMetrics];
}
_ascent = ascent;
}
- (CGFloat)descent
{
if (!_didCalculateMetrics)
{
[self _calculateMetrics];
}
return _descent;
}
- (CGFloat)leading
{
if (!_didCalculateMetrics)
{
[self _calculateMetrics];
}
return _leading;
}
- (CGFloat)underlineOffset
{
if (!_hasScannedGlyphRunsForValues)
{
[self _scanGlyphRunsForValues];
}
return _underlineOffset;
}
- (CGFloat)lineHeight
{
if (!_hasScannedGlyphRunsForValues)
{
[self _scanGlyphRunsForValues];
}
return _lineHeight;
}
- (DTCoreTextParagraphStyle *)paragraphStyle
{
// get paragraph style from any glyph
DTCoreTextGlyphRun *lastRun = [self.glyphRuns lastObject];
NSDictionary *attributes = lastRun.attributes;
return [attributes paragraphStyle];
}
- (NSArray *)textBlocks
{
// get text blocks from any glyph
DTCoreTextGlyphRun *lastRun = [self.glyphRuns lastObject];
NSDictionary *attributes = lastRun.attributes;
return [attributes objectForKey:DTTextBlocksAttribute];
}
- (CGFloat)trailingWhitespaceWidth
{
if (!_didCalculateMetrics)
{
[self _calculateMetrics];
}
return _trailingWhitespaceWidth;
}
- (BOOL)writingDirectionIsRightToLeft
{
if (_needsToDetectWritingDirection)
{
if ([self.glyphRuns count])
{
DTCoreTextGlyphRun *firstRun = [self.glyphRuns objectAtIndex:0];
_writingDirectionIsRightToLeft = [firstRun writingDirectionIsRightToLeft];
}
}
return _writingDirectionIsRightToLeft;
}
- (void)setWritingDirectionIsRightToLeft:(BOOL)writingDirectionIsRightToLeft
{
_writingDirectionIsRightToLeft = writingDirectionIsRightToLeft;
_needsToDetectWritingDirection = NO;
}
@synthesize frame =_frame;
@synthesize glyphRuns = _glyphRuns;
@synthesize ascent = _ascent;
@synthesize descent = _descent;
@synthesize leading = _leading;
@synthesize trailingWhitespaceWidth = _trailingWhitespaceWidth;
@synthesize baselineOrigin = _baselineOrigin;
@synthesize writingDirectionIsRightToLeft = _writingDirectionIsRightToLeft;
@synthesize stringLocationOffset = _stringLocationOffset;
@end
@@ -1,69 +0,0 @@
//
// DTCoreTextLayouter.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/24/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE
#import <CoreText/CoreText.h>
#elif TARGET_OS_MAC
#import <ApplicationServices/ApplicationServices.h>
#endif
#import "DTCoreTextLayoutFrame.h"
#import "DTCoreTextLayoutLine.h"
#import "DTCoreTextGlyphRun.h"
/**
This class owns an attributed string and is able to create layoutFrames for certain ranges in this string. Optionally it caches these layout frames.
*/
@interface DTCoreTextLayouter : NSObject
/**
@name Creating a Layouter
*/
/**
Designated Initializer. Creates a new Layouter with an attributed string
@param attributedString The `NSAttributedString` to layout for
@returns An initialized layouter
*/
- (id)initWithAttributedString:(NSAttributedString *)attributedString;
/**
@name Creating Layout Frames
*/
/**
Creates a layout frame with a given rectangle and string range. The layouter fills the layout frame with as many lines as fit. You can query [DTCoreTextLayoutFrame visibleStringRange] for the range the fits and create another layout frame that continues the text from there to create multiple pages, for example for an e-book.
@param frame The rectangle to fill with text
@param range The string range to fill, pass {0,0} for the entire string (as much as fits)
*/
- (DTCoreTextLayoutFrame *)layoutFrameWithRect:(CGRect)frame range:(NSRange)range;
/**
If set to `YES` then the receiver will cache layout frames generated with layoutFrameWithRect:range: for a given rect
*/
@property (nonatomic, assign) BOOL shouldCacheLayoutFrames;
/**
@name Getting Information
*/
/**
The attributed string that the layouter currently owns
*/
@property (nonatomic, strong) NSAttributedString *attributedString;
/**
The internal framesetter of the receiver
*/
@property (nonatomic, readonly) CTFramesetterRef framesetter;
@end
@@ -1,153 +0,0 @@
//
// DTCoreTextLayouter.m
// DTCoreText
//
// Created by Oliver Drobnik on 1/24/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTCoreTextLayouter.h"
@interface DTCoreTextLayouter ()
@property (nonatomic, strong) NSMutableArray *frames;
- (void)_discardFramesetter;
@end
@implementation DTCoreTextLayouter
{
CTFramesetterRef _framesetter;
NSAttributedString *_attributedString;
BOOL _shouldCacheLayoutFrames;
NSCache *_layoutFrameCache;
}
- (id)initWithAttributedString:(NSAttributedString *)attributedString
{
if ((self = [super init]))
{
if (!attributedString)
{
return nil;
}
self.attributedString = attributedString;
}
return self;
}
- (void)dealloc
{
[self _discardFramesetter];
}
- (DTCoreTextLayoutFrame *)layoutFrameWithRect:(CGRect)frame range:(NSRange)range
{
DTCoreTextLayoutFrame *newFrame = nil;
NSString *cacheKey = nil;
// need to have a non zero
if (!(frame.size.width > 0 && frame.size.height > 0))
{
return nil;
}
if (_shouldCacheLayoutFrames)
{
cacheKey = [NSString stringWithFormat:@"%lud-%@-%@", (unsigned long)[_attributedString hash], NSStringFromCGRect(frame), NSStringFromRange(range)];
DTCoreTextLayoutFrame *cachedLayoutFrame = [_layoutFrameCache objectForKey:cacheKey];
if (cachedLayoutFrame)
{
return cachedLayoutFrame;
}
}
@autoreleasepool
{
newFrame = [[DTCoreTextLayoutFrame alloc] initWithFrame:frame layouter:self range:range];
};
if (newFrame && _shouldCacheLayoutFrames)
{
[_layoutFrameCache setObject:newFrame forKey:cacheKey];
}
return newFrame;
}
- (void)_discardFramesetter
{
// framesetter needs to go
if (_framesetter)
{
CFRelease(_framesetter);
_framesetter = NULL;
}
}
#pragma mark Properties
- (CTFramesetterRef)framesetter
{
@synchronized(self)
{
if (!_framesetter)
{
_framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.attributedString);
}
return _framesetter;
}
}
- (void)setAttributedString:(NSAttributedString *)attributedString
{
@synchronized(self)
{
if (_attributedString != attributedString)
{
_attributedString = attributedString;
[self _discardFramesetter];
// clear the cache
[_layoutFrameCache removeAllObjects];
}
}
}
- (NSAttributedString *)attributedString
{
return _attributedString;
}
- (void)setShouldCacheLayoutFrames:(BOOL)shouldCacheLayoutFrames
{
if (_shouldCacheLayoutFrames != shouldCacheLayoutFrames)
{
_shouldCacheLayoutFrames = shouldCacheLayoutFrames;
if (shouldCacheLayoutFrames)
{
_layoutFrameCache = [[NSCache alloc] init];
}
else
{
_layoutFrameCache = nil;
}
}
}
@synthesize attributedString = _attributedString;
@synthesize framesetter = _framesetter;
@synthesize shouldCacheLayoutFrames = _shouldCacheLayoutFrames;
@end
@@ -1,17 +0,0 @@
//
// DTCoreTextMacros.h
// DTCoreText
//
// Created by Jean-Charles BERTIN on 5/28/14.
// Copyright (c) 2014 Axinoe. All rights reserved.
//
#import "DTCompatibility.h"
#ifndef DT_RETURNS_INNER_POINTER
#if __has_attribute(objc_returns_inner_pointer)
#define DT_RETURNS_INNER_POINTER __attribute__((objc_returns_inner_pointer))
#else
#define DT_RETURNS_INNER_POINTER
#endif
#endif
@@ -1,212 +0,0 @@
//
// DTCoreTextParagraphStyle.h
// DTCoreText
//
// Created by Oliver Drobnik on 4/14/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>
/**
`DTCoreTextParagraphStyle` encapsulates the paragraph or ruler attributes used by the NSAttributedString classes on iOS. It is a replacement for `NSParagraphStyle` which is not implemented on iOS.
Since `NSAttributedString` instances use CTParagraphStyle object there are methods to bridge from and to these. Because of this distinction there is no need for a mutable variant of this class.
*/
@interface DTCoreTextParagraphStyle : NSObject <NSCopying>
/**
@name Creating a DTCoreTextParagraphStyle
*/
/**
Returns the default paragraph style.
*/
+ (DTCoreTextParagraphStyle *)defaultParagraphStyle;
/**
@name Bridging to and from CTParagraphStyle
*/
/**
Create a new paragraph style instance from a `CTParagraphStyle`.
@param ctParagraphStyle the `CTParagraphStyle` from which to copy this new style's attributes.
*/
+ (DTCoreTextParagraphStyle *)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle;
/**
Create a new paragraph style instance from a `CTParagraphStyle`.
@param ctParagraphStyle the `CTParagraphStyle` from which to copy this new style's attributes.
*/
- (id)initWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle;
/**
Create a new `CTParagraphStyle` from the receiver for use as attribute in `NSAttributedString`
@returns The `CTParagraphStyle` based on the receiver's attributes.
*/
- (CTParagraphStyleRef)createCTParagraphStyle;
/**
@name Bridging to and from NSParagraphStyle
*/
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
/**
Create a new paragraph style instance from an `NSParagraphStyle`.
Note: on iOS no tab stops are supported.
@param paragraphStyle the `NSParagraphStyle` from which to copy this new style's attributes.
*/
+ (DTCoreTextParagraphStyle *)paragraphStyleWithNSParagraphStyle:(NSParagraphStyle *)paragraphStyle;
/**
Create a new `NSParagraphStyle` from the receiver for use as attribute in `NSAttributedString`.
Note: This method is requires iOS 6 or greater. This does not support tab stops.
@returns The `NSParagraphStyle` based on the receiver's attributes.
*/
- (NSParagraphStyle *)NSParagraphStyle;
#endif
/**-------------------------------------------------------------------------------------
@name Accessing Style Information
---------------------------------------------------------------------------------------
*/
/**
The indentation of the first line of the receiver.
*/
@property (nonatomic, assign) CGFloat firstLineHeadIndent;
/**
The document-wide default tab interval.
The default tab interval in points. Tabs after the last specified in tabStops are placed at integer multiples of this distance (if positive). Default return value is 0.0.
*/
@property (nonatomic, assign) CGFloat defaultTabInterval;
/**
The distance between the paragraphs top and the beginning of its text content.
*/
@property (nonatomic, assign) CGFloat paragraphSpacingBefore;
/**
The space after the end of the paragraph.
*/
@property (nonatomic, assign) CGFloat paragraphSpacing;
/**
The line height multiple.
Internally line height multiples get converted into minimum and maximum line height.
*/
@property (nonatomic, assign) CGFloat lineHeightMultiple;
/**
The minimum height in points that any line in the receiver will occupy, regardless of the font size or size of any attached graphic. This value is always nonnegative.
*/
@property (nonatomic, assign) CGFloat minimumLineHeight;
/**
The maximum height in points that any line in the receiver will occupy, regardless of the font size or size of any attached graphic. This value is always nonnegative. The default value is 0.
*/
@property (nonatomic, assign) CGFloat maximumLineHeight;
/**
The distance in points from the margin of a text container to the end of lines.
@note This value is negative if it is to be measured from the trailing margin, positive if measured from the same margin as the headIndent.
*/
@property (nonatomic, assign) CGFloat tailIndent;
/**
The distance in points from the leading margin of a text container to the beginning of lines other than the first. This value is always nonnegative.
*/
@property (nonatomic, assign) CGFloat headIndent;
/**
The text alignment of the receiver.
Natural text alignment is realized as left or right alignment depending on the line sweep direction of the first script contained in the paragraph.
*/
@property (nonatomic, assign) CTTextAlignment alignment;
/**
The base writing direction for the receiver.
*/
@property (nonatomic, assign) CTWritingDirection baseWritingDirection;
/**-------------------------------------------------------------------------------------
@name Setting Tab Stops
---------------------------------------------------------------------------------------
*/
/**
The CTTextTab objects, sorted by location, that define the tab stops for the paragraph style.
*/
@property (nonatomic, copy) NSArray *tabStops;
/**
Adds a tab stop to the receiver.
@param position the tab stop position
@param alignment the tab alignment for this tab stop
*/
- (void)addTabStopAtPosition:(CGFloat)position alignment:(CTTextAlignment)alignment;
/**-------------------------------------------------------------------------------------
@name Interacting with CSS
---------------------------------------------------------------------------------------
*/
/**
Create a representation suitable for CSS.
@returns A string with the receiver's style encoded as CSS.
*/
- (NSString *)cssStyleRepresentation;
/**-------------------------------------------------------------------------------------
@name Setting Text Lists
---------------------------------------------------------------------------------------
*/
/**
Text lists containing the paragraph, nested from outermost to innermost. Each text list is a DTCSSListStyle object.
*/
@property (nonatomic, copy) NSArray *textLists;
/**-------------------------------------------------------------------------------------
@name Setting Text Blocks
---------------------------------------------------------------------------------------
*/
/**
Text blocks containing the paragraph, nested from outermost to innermost, to array. Each text block is a DTTextBlock object.
*/
@property (nonatomic, copy) NSArray *textBlocks;
@end
@@ -1,664 +0,0 @@
//
// DTCoreTextParagraphStyle.m
// DTCoreText
//
// Created by Oliver Drobnik on 4/14/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCoreTextParagraphStyle.h"
#import "DTTextBlock.h"
#import "DTCSSListStyle.h"
#import <DTFoundation/DTWeakSupport.h>
#import "DTCoreTextFunctions.h"
@implementation DTCoreTextParagraphStyle
{
CGFloat _firstLineHeadIndent;
CGFloat _defaultTabInterval;
CGFloat _paragraphSpacingBefore;
CGFloat _paragraphSpacing;
CGFloat _headIndent;
CGFloat _tailIndent;
CGFloat _lineHeightMultiple;
CGFloat _minimumLineHeight;
CGFloat _maximumLineHeight;
CTTextAlignment _alignment;
CTWritingDirection _baseWritingDirection;
NSMutableArray *_tabStops;
}
+ (DTCoreTextParagraphStyle *)defaultParagraphStyle
{
return [[DTCoreTextParagraphStyle alloc] init];
}
+ (DTCoreTextParagraphStyle *)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle
{
return [[DTCoreTextParagraphStyle alloc] initWithCTParagraphStyle:ctParagraphStyle];
}
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
+ (DTCoreTextParagraphStyle *)paragraphStyleWithNSParagraphStyle:(NSParagraphStyle *)paragraphStyle
{
if ( ! paragraphStyle) {
paragraphStyle = [NSParagraphStyle defaultParagraphStyle];
}
DTCoreTextParagraphStyle *retStyle = [[DTCoreTextParagraphStyle alloc] init];
retStyle.firstLineHeadIndent = paragraphStyle.firstLineHeadIndent;
retStyle.headIndent = paragraphStyle.headIndent;
retStyle.paragraphSpacing = paragraphStyle.paragraphSpacing;
retStyle.paragraphSpacingBefore = paragraphStyle.paragraphSpacingBefore;
retStyle.lineHeightMultiple = paragraphStyle.lineHeightMultiple;
retStyle.minimumLineHeight = paragraphStyle.minimumLineHeight;
retStyle.maximumLineHeight = paragraphStyle.maximumLineHeight;
retStyle.alignment = DTNSTextAlignmentToCTTextAlignment(paragraphStyle.alignment);
switch (paragraphStyle.baseWritingDirection)
{
case NSWritingDirectionNatural:
{
retStyle.baseWritingDirection = kCTWritingDirectionNatural;
break;
}
case NSWritingDirectionLeftToRight:
{
retStyle.baseWritingDirection = kCTWritingDirectionLeftToRight;
break;
}
case NSWritingDirectionRightToLeft:
{
retStyle.baseWritingDirection = kCTWritingDirectionRightToLeft;
break;
}
}
#if DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS
if (NSClassFromString(@"NSTextTab") && [NSParagraphStyle instancesRespondToSelector:@selector(tabStops)])
{
NSArray *tabStops = [paragraphStyle valueForKey:@"tabStops"];
NSMutableArray *tmpArray = [NSMutableArray array];
for (NSTextTab *textTab in tabStops)
{
CTTextAlignment alignment = DTNSTextAlignmentToCTTextAlignment(textTab.alignment);
CGFloat location = textTab.location;
CTTextTabRef tab = CTTextTabCreate(alignment, location, NULL);
if (tab)
{
[tmpArray addObject:(__bridge id)(tab)];
CFRelease(tab);
}
}
if ([tmpArray count])
{
retStyle.tabStops = tmpArray;
}
}
retStyle.defaultTabInterval = paragraphStyle.defaultTabInterval;
#endif
return retStyle;
}
#endif
- (id)init
{
if ((self = [super init]))
{
// defaults
_firstLineHeadIndent = 0.0;
_defaultTabInterval = 36.0;
_baseWritingDirection = kCTWritingDirectionNatural;
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
_alignment = kCTTextAlignmentNatural;
#else
_alignment = kCTNaturalTextAlignment;
#endif
_lineHeightMultiple = 0.0;
_minimumLineHeight = 0.0;
_maximumLineHeight = 0.0;
_paragraphSpacing = 0.0;
}
return self;
}
- (id)initWithCTParagraphStyle:(CTParagraphStyleRef)ctParagraphStyle
{
if ((self = [super init]))
{
// text alignment
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierAlignment,sizeof(_alignment), &_alignment);
// indents
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(_firstLineHeadIndent), &_firstLineHeadIndent);
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(_headIndent), &_headIndent);
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(_tailIndent), &_tailIndent);
// paragraph spacing
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(_paragraphSpacing), &_paragraphSpacing);
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore,sizeof(_paragraphSpacingBefore), &_paragraphSpacingBefore);
// tab stops
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(_defaultTabInterval), &_defaultTabInterval);
CFArrayRef stops; // Could use a CFArray too, leave as a reminder how to do this in the future
if (CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierTabStops, sizeof(stops), &stops))
{
self.tabStops = (__bridge NSArray *) stops;
}
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(_baseWritingDirection), &_baseWritingDirection);
// line height
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(_minimumLineHeight), &_minimumLineHeight);
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(_maximumLineHeight), &_maximumLineHeight);
CTParagraphStyleGetValueForSpecifier(ctParagraphStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(_lineHeightMultiple), &_lineHeightMultiple);
}
return self;
}
- (CTParagraphStyleRef)createCTParagraphStyle
{
// This just makes it that much easier to track down memory issues with tabstops
CFArrayRef stops = _tabStops ? CFArrayCreateCopy (NULL, (__bridge CFArrayRef)_tabStops) : NULL;
CTParagraphStyleSetting settings[] =
{
{kCTParagraphStyleSpecifierAlignment, sizeof(_alignment), &_alignment},
{kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(_firstLineHeadIndent), &_firstLineHeadIndent},
{kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(_defaultTabInterval), &_defaultTabInterval},
{kCTParagraphStyleSpecifierTabStops, sizeof(stops), &stops},
{kCTParagraphStyleSpecifierParagraphSpacing, sizeof(_paragraphSpacing), &_paragraphSpacing},
{kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(_paragraphSpacingBefore), &_paragraphSpacingBefore},
{kCTParagraphStyleSpecifierHeadIndent, sizeof(_headIndent), &_headIndent},
{kCTParagraphStyleSpecifierTailIndent, sizeof(_tailIndent), &_tailIndent},
{kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(_baseWritingDirection), &_baseWritingDirection},
{kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(_lineHeightMultiple), &_lineHeightMultiple},
{kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(_minimumLineHeight), &_minimumLineHeight},
{kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(_maximumLineHeight), &_maximumLineHeight}
};
CTParagraphStyleRef ret = CTParagraphStyleCreate(settings, 12);
if (stops)
{
CFRelease(stops);
}
return ret;
}
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
- (NSParagraphStyle *)NSParagraphStyle
{
NSMutableParagraphStyle *mps = [[NSMutableParagraphStyle alloc] init];
[mps setFirstLineHeadIndent:_firstLineHeadIndent];
[mps setParagraphSpacing:_paragraphSpacing];
[mps setParagraphSpacingBefore:_paragraphSpacingBefore];
[mps setHeadIndent:_headIndent];
[mps setTailIndent:_tailIndent];
[mps setMinimumLineHeight:_minimumLineHeight];
[mps setMaximumLineHeight:_maximumLineHeight];
[mps setLineHeightMultiple:_lineHeightMultiple];
[mps setAlignment:DTNSTextAlignmentFromCTTextAlignment(_alignment)];
switch (_baseWritingDirection)
{
case kCTWritingDirectionNatural:
{
[mps setBaseWritingDirection:NSWritingDirectionNatural];
break;
}
case kCTWritingDirectionLeftToRight:
{
[mps setBaseWritingDirection:NSWritingDirectionLeftToRight];
break;
}
case kCTWritingDirectionRightToLeft:
{
[mps setBaseWritingDirection:NSWritingDirectionRightToLeft];
break;
}
}
#if DTCORETEXT_SUPPORT_NSPARAGRAPHSTYLE_TABS
if (NSClassFromString(@"NSTextTab") && [NSParagraphStyle instancesRespondToSelector:@selector(tabStops)])
{
NSMutableArray *tabs = [NSMutableArray array];
for (id object in _tabStops)
{
CTTextTabRef tab = (__bridge CTTextTabRef)(object);
CTTextAlignment alignment = CTTextTabGetAlignment(tab);
NSTextAlignment nsAlignment = DTNSTextAlignmentFromCTTextAlignment(alignment);
CGFloat location = (CGFloat)CTTextTabGetLocation(tab);
NSTextTab *textTab = [[NSTextTab alloc] initWithTextAlignment:nsAlignment location:location options:[NSDictionary dictionary]];
if (!textTab)
{
break;
}
[tabs addObject:textTab];
}
if ([tabs count])
{
[mps setValue:tabs forKey:@"tabStops"];
}
mps.defaultTabInterval = _defaultTabInterval;
}
#endif
return (NSParagraphStyle *)mps;
}
#endif
- (void)addTabStopAtPosition:(CGFloat)position alignment:(CTTextAlignment)alignment
{
CTTextTabRef tab = CTTextTabCreate(alignment, position, NULL);
if(tab)
{
if (!_tabStops)
{
_tabStops = [[NSMutableArray alloc] init];
}
[_tabStops addObject:CFBridgingRelease(tab)];
}
}
#pragma mark HTML Encoding
// representation of this paragraph style in css (as far as possible)
- (NSString *)cssStyleRepresentation
{
NSMutableString *retString = [NSMutableString string];
switch (_alignment)
{
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
case kCTTextAlignmentLeft:
#else
case kCTLeftTextAlignment:
#endif
[retString appendString:@"text-align:left;"];
break;
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
case kCTTextAlignmentRight:
#else
case kCTRightTextAlignment:
#endif
[retString appendString:@"text-align:right;"];
break;
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
case kCTTextAlignmentCenter:
#else
case kCTCenterTextAlignment:
#endif
[retString appendString:@"text-align:center;"];
break;
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
case kCTTextAlignmentJustified:
#else
case kCTJustifiedTextAlignment:
#endif
[retString appendString:@"text-align:justify;"];
break;
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
case kCTTextAlignmentNatural:
#else
case kCTNaturalTextAlignment:
#endif
// no output, this is default
break;
}
if (_lineHeightMultiple!=0 && _lineHeightMultiple!=1.0f)
{
NSNumber *number = DTNSNumberFromCGFloat(_lineHeightMultiple);
[retString appendFormat:@"line-height:%@em;", number];
}
switch (_baseWritingDirection)
{
case kCTWritingDirectionRightToLeft:
[retString appendString:@"direction:rtl;"];
break;
case kCTWritingDirectionLeftToRight:
[retString appendString:@"direction:ltr;"];
break;
case kCTWritingDirectionNatural:
// no output, this is default
break;
}
// Spacing at the bottom
if (_paragraphSpacing!=0.0f)
{
NSNumber *number = DTNSNumberFromCGFloat(_paragraphSpacing);
[retString appendFormat:@"margin-bottom:%@px;", number];
}
// Spacing at the top
if (_paragraphSpacingBefore!=0.0f)
{
NSNumber *number = DTNSNumberFromCGFloat(_paragraphSpacingBefore);
[retString appendFormat:@"margin-top:%@px;", number];
}
// Spacing at the left
if (_headIndent!=0.0f)
{
NSNumber *number = DTNSNumberFromCGFloat(_headIndent);
[retString appendFormat:@"margin-left:%@px;", number];
}
// Spacing at the right
if (_tailIndent!=0.0f)
{
// tail indent is negative if from trailing margin
NSNumber *number = DTNSNumberFromCGFloat(-_tailIndent);
[retString appendFormat:@"margin-right:%@px;", number];
}
// return nil if no content
if ([retString length])
{
return retString;
}
else
{
return nil;
}
}
#pragma mark Copying
- (id)copyWithZone:(NSZone *)zone
{
DTCoreTextParagraphStyle *newObject = [[DTCoreTextParagraphStyle allocWithZone:zone] init];
newObject.firstLineHeadIndent = self.firstLineHeadIndent;
newObject.tailIndent = self.tailIndent;
newObject.defaultTabInterval = self.defaultTabInterval;
newObject.paragraphSpacing = self.paragraphSpacing;
newObject.paragraphSpacingBefore = self.paragraphSpacingBefore;
newObject.lineHeightMultiple = self.lineHeightMultiple;
newObject.minimumLineHeight = self.minimumLineHeight;
newObject.maximumLineHeight = self.maximumLineHeight;
newObject.headIndent = self.headIndent;
newObject.alignment = self.alignment;
newObject.baseWritingDirection = self.baseWritingDirection;
newObject.tabStops = self.tabStops; // copy
newObject.textLists = self.textLists; //copy
newObject.textBlocks = self.textBlocks; //copy
return newObject;
}
#pragma mark - Comparing
- (BOOL)isEqual:(id)object
{
if (object == self)
{
return YES;
}
if (!object)
{
return NO;
}
if (![object isKindOfClass:[DTCoreTextParagraphStyle class]])
{
return NO;
}
DTCoreTextParagraphStyle *otherStyle = object;
if (_firstLineHeadIndent != otherStyle->_firstLineHeadIndent)
{
return NO;
}
if (_headIndent != otherStyle->_headIndent)
{
return NO;
}
if (_tailIndent != otherStyle->_tailIndent)
{
return NO;
}
if (_defaultTabInterval != otherStyle->_defaultTabInterval)
{
return NO;
}
if (_paragraphSpacing != otherStyle->_paragraphSpacing)
{
return NO;
}
if (_paragraphSpacingBefore != otherStyle->_paragraphSpacingBefore)
{
return NO;
}
if (_lineHeightMultiple != otherStyle->_lineHeightMultiple)
{
return NO;
}
if (_minimumLineHeight != otherStyle->_minimumLineHeight)
{
return NO;
}
if (_maximumLineHeight != otherStyle->_maximumLineHeight)
{
return NO;
}
if (_alignment != otherStyle->_alignment)
{
return NO;
}
if (_baseWritingDirection != otherStyle->_baseWritingDirection)
{
return NO;
}
if (_textLists && ![_textLists isEqualToArray:otherStyle->_textLists])
{
return NO;
}
if (_textBlocks && ![_textBlocks isEqualToArray:otherStyle->_textBlocks])
{
return NO;
}
if (_tabStops && ![_tabStops isEqualToArray:otherStyle->_tabStops])
{
return NO;
}
return YES;
}
#pragma mark Properties
- (void)setTabStops:(NSArray *)tabStops
{
if (tabStops != _tabStops)
{
_tabStops = [tabStops mutableCopy]; // keep mutability
}
}
- (void)setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent
{
if (_firstLineHeadIndent != firstLineHeadIndent)
{
_firstLineHeadIndent = firstLineHeadIndent;
}
}
- (void)setDefaultTabInterval:(CGFloat)defaultTabInterval
{
if (_defaultTabInterval != defaultTabInterval)
{
_defaultTabInterval = defaultTabInterval;
}
}
- (void)setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore
{
if (_paragraphSpacingBefore != paragraphSpacingBefore)
{
_paragraphSpacingBefore = paragraphSpacingBefore;
}
}
- (void)setParagraphSpacing:(CGFloat)paragraphSpacing
{
if (_paragraphSpacing != paragraphSpacing)
{
_paragraphSpacing = paragraphSpacing;
}
}
- (void)setLineHeightMultiple:(CGFloat)lineHeightMultiple
{
if (_lineHeightMultiple != lineHeightMultiple)
{
_lineHeightMultiple = lineHeightMultiple;
}
}
- (void)setMinimumLineHeight:(CGFloat)minimumLineHeight
{
if (_minimumLineHeight != minimumLineHeight)
{
_minimumLineHeight = minimumLineHeight;
}
}
- (void)setMaximumLineHeight:(CGFloat)maximumLineHeight
{
if (_maximumLineHeight != maximumLineHeight)
{
_maximumLineHeight = maximumLineHeight;
}
}
- (void)setHeadIndent:(CGFloat)headIndent
{
if (_headIndent != headIndent)
{
_headIndent = headIndent;
}
}
- (void)setTailIndent:(CGFloat)tailIndent
{
if (_tailIndent != tailIndent)
{
_tailIndent = tailIndent;
}
}
- (void)setAlignment:(CTTextAlignment)alignment
{
if (_alignment != alignment)
{
_alignment = alignment;
}
}
- (void)setTextLists:(NSArray *)textLists
{
if (_textLists != textLists)
{
_textLists = [textLists copy];
}
}
- (void)setTextBlocks:(NSArray *)textBlocks
{
if (_textBlocks != textBlocks)
{
_textBlocks = [textBlocks copy];
}
}
- (void)setBaseWritingDirection:(CTWritingDirection)baseWritingDirection
{
if (_baseWritingDirection != baseWritingDirection)
{
_baseWritingDirection = baseWritingDirection;
}
}
@synthesize firstLineHeadIndent = _firstLineHeadIndent;
@synthesize defaultTabInterval = _defaultTabInterval;
@synthesize paragraphSpacingBefore = _paragraphSpacingBefore;
@synthesize paragraphSpacing = _paragraphSpacing;
@synthesize lineHeightMultiple = _lineHeightMultiple;
@synthesize minimumLineHeight = _minimumLineHeight;
@synthesize maximumLineHeight = _maximumLineHeight;
@synthesize headIndent = _headIndent;
@synthesize tailIndent = _tailIndent;
@synthesize alignment = _alignment;
@synthesize textLists = _textLists;
@synthesize textBlocks = _textBlocks;
@synthesize baseWritingDirection = _baseWritingDirection;
@synthesize tabStops = _tabStops;
@end
@@ -1,24 +0,0 @@
//
// DTDictationPlaceholderTextAttachment.h
// DTCoreText
//
// Created by Oliver Drobnik on 06.02.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTTextAttachment.h"
/**
This is a special subclass of DTTextAttachment used to represent the dictation placeholder.
When encountering such an element DTAttributedTextContentView does not call the delegate to provide a subclass but automatically creates and adds a DTDictationPlaceholderView.
*/
@interface DTDictationPlaceholderTextAttachment : DTTextAttachment
/**
The string that inserting the dictation placeholder replaced, used for Undoing
*/
@property (nonatomic, retain) NSAttributedString *replacedAttributedString;
@end
@@ -1,59 +0,0 @@
//
// DTDictationPlaceholderTextAttachment.m
// DTCoreText
//
// Created by Oliver Drobnik on 06.02.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTDictationPlaceholderTextAttachment.h"
@implementation DTDictationPlaceholderTextAttachment
{
NSAttributedString *_replacedAttributedString;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_replacedAttributedString = [aDecoder decodeObjectForKey:@"replacedAttributedString"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeObject:_replacedAttributedString forKey:@"replacedAttributedString"];
}
// if you change any of these then also make sure to adjust the sizes in DTDictationPlaceholderTextAttachment
#define DOT_WIDTH 10.0f
#define DOT_DISTANCE 2.5f
#define DOT_OUTSIDE_MARGIN 3.0f
// several hard-coded items
- (CGSize)displaySize
{
return CGSizeMake(DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH*3.0f + DOT_DISTANCE*2.0f, DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH);
}
- (CGSize)originalSize
{
return [self displaySize];
}
- (CGFloat)ascentForLayout
{
return self.displaySize.height;
}
- (CGFloat)descentForLayout
{
return 0.0f;
}
#pragma mark - Properties
@synthesize replacedAttributedString = _replacedAttributedString;
@end
@@ -1,33 +0,0 @@
//
// DTDictationPlaceholderView.h
// DTRichTextEditor
//
// Created by Oliver Drobnik on 05.02.13.
// Copyright (c) 2013 Cocoanetics. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <UIKit/UIKit.h>
/**
A dictation placeholder to display in editors between the time the recording is complete until a recognized response is received.
*/
@interface DTDictationPlaceholderView : UIView
/**
Creates an appropriately sized DTDictationPlaceholderView with 3 animated purple dots
*/
+ (DTDictationPlaceholderView *)placeholderView;
/**
The context of the receiver. This can be any object, for example the selection range to replace with the dictation result text
*/
@property (nonatomic, strong) id context;
@end
#endif
@@ -1,123 +0,0 @@
//
// DTDictationPlaceholderView.m
// DTRichTextEditor
//
// Created by Oliver Drobnik on 05.02.13.
// Copyright (c) 2013 Cocoanetics. All rights reserved.
//
#import "DTDictationPlaceholderView.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
// if you change any of these then also make sure to adjust the sizes in DTDictationPlaceholderTextAttachment
#define DOT_WIDTH 10.0f
#define DOT_DISTANCE 2.5f
#define DOT_OUTSIDE_MARGIN 3.0f
@implementation DTDictationPlaceholderView
{
NSUInteger _phase;
NSTimer *_phaseTimer;
}
+ (DTDictationPlaceholderView *)placeholderView;
{
return [[DTDictationPlaceholderView alloc] initWithFrame:CGRectZero];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.backgroundColor = [UIColor clearColor];
[self sizeToFit];
}
return self;
}
- (CGSize)sizeThatFits:(CGSize)size
{
return CGSizeMake(DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH*3.0f + DOT_DISTANCE*2.0f, DOT_OUTSIDE_MARGIN*2.0f + DOT_WIDTH);
}
- (UIColor *)_lightDotColor
{
return [UIColor colorWithRed:(CGFloat)(238.0/255.0) green:(CGFloat)(128.0/255.0) blue:(CGFloat)(238.0/255.0) alpha:1.0];
}
- (UIColor *)_darkDotColor
{
return [UIColor colorWithRed:(CGFloat)(191.0/255.0) green:(CGFloat)(51.0/255.0) blue:(CGFloat)(191.0/255.0) alpha:1.0];
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
[_phaseTimer invalidate];
_phaseTimer = nil;
if (newSuperview)
{
_phaseTimer = [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(_phaseTimerTick:) userInfo:nil repeats:YES];
}
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect dotRect = CGRectMake(DOT_OUTSIDE_MARGIN, 4, DOT_WIDTH, DOT_WIDTH);
if (_phase==0)
{
CGContextSetFillColorWithColor(ctx, [self _darkDotColor].CGColor);
}
else
{
CGContextSetFillColorWithColor(ctx, [self _lightDotColor].CGColor);
}
CGContextFillEllipseInRect(ctx, dotRect);
dotRect.origin.x = DOT_DISTANCE + CGRectGetMaxX(dotRect);
if (_phase==1)
{
CGContextSetFillColorWithColor(ctx, [self _darkDotColor].CGColor);
}
else
{
CGContextSetFillColorWithColor(ctx, [self _lightDotColor].CGColor);
}
CGContextFillEllipseInRect(ctx, dotRect);
dotRect.origin.x = DOT_DISTANCE + CGRectGetMaxX(dotRect);
if (_phase==2)
{
CGContextSetFillColorWithColor(ctx, [self _darkDotColor].CGColor);
}
else
{
CGContextSetFillColorWithColor(ctx, [self _lightDotColor].CGColor);
}
CGContextFillEllipseInRect(ctx, dotRect);
}
- (void)_phaseTimerTick:(id)sender
{
_phase = (_phase+1)%3;
[self setNeedsDisplay];
}
@end
#endif
@@ -1,95 +0,0 @@
//
// DTHTMLAttributedStringBuilder.h
// DTCoreText
//
// Created by Oliver Drobnik on 21.01.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import <DTFoundation/DTHTMLParser.h>
@class DTHTMLElement;
/**
The block that gets executed whenever an element is flushed to the output string
*/
typedef void(^DTHTMLAttributedStringBuilderWillFlushCallback)(DTHTMLElement *);
/**
The block that gets executed whenever html tag parsing error
*/
typedef void(^DTHTMLAttributedStringBuilderParseErrorCallback)(NSAttributedString *attr, NSError *);
/**
Class for building an `NSAttributedString` from an HTML document.
*/
@interface DTHTMLAttributedStringBuilder : NSObject <DTHTMLParserDelegate>
/**
@name Creating an Attributed String Builder
*/
/**
Initializes and returns a new `NSAttributedString` object from the HTML contained in the given object and base URL.
Options can be:
- DTMaxImageSize: the maximum CGSize that a text attachment can fill
- DTDefaultFontFamily: the default font family to use instead of Times New Roman
- DTDefaultFontName: the default font face to use instead of Times New Roman
- DTDefaultFontSize: the default font size to use instead of 12
- DTDefaultFontDescriptor: the default font descriptor. This supercedes font family/name
- DTDefaultTextColor: the default text color
- DTDefaultLinkColor: the default color for hyperlink text
- DTDefaultLinkDecoration: the default decoration for hyperlinks
- DTDefaultLinkHighlightColor: the color to show while the hyperlink is highlighted
- DTDefaultTextAlignment: the default text alignment for paragraphs
- DTDefaultLineHeightMultiplier: The multiplier for line heights
- DTDefaultFirstLineHeadIndent: The default indent for left margin on first line
- DTDefaultHeadIndent: The default indent for left margin except first line
- DTDefaultListIndent: The amount by which lists are indented
- DTDefaultStyleSheet: The default style sheet to use
- DTUseiOS6Attributes: use iOS 6 attributes for building (UITextView compatible)
- DTWillFlushBlockCallBack: a block to be executed whenever content is flushed to the output string
- DTIgnoreInlineStylesOption: All inline style information is being ignored and only style blocks used
@param data The data in HTML format from which to create the attributed string.
@param options Specifies how the document should be loaded. Contains values described in NSAttributedString(HTML).
@param docAttributes Currently not in use.
@returns Returns an initialized object, or `nil` if the data cant be decoded.
*/
- (id)initWithHTML:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary * __autoreleasing*)docAttributes;
/**
@name Generating Attributed Strings
*/
/**
Creates the attributed string when called the first time.
@returns An `NSAttributedString` representing the HTML document passed in the initializer.
*/
- (NSAttributedString *)generatedAttributedString;
/**
This block is called before the element is written to the output attributed string
*/
@property (nonatomic, copy) DTHTMLAttributedStringBuilderWillFlushCallback willFlushCallback;
/**
The block that gets executed whenever html tag parsing error
*/
@property (nonatomic, copy) DTHTMLAttributedStringBuilderParseErrorCallback parseErrorCallback;
/**
Setting this property to `YES` causes the tree of parse nodes to be preserved until the end of the generation process. This allows to output the HTML structure of the document for debugging.
*/
@property (nonatomic, assign) BOOL shouldKeepDocumentNodeTree;
/**
This func can abort AttributedString building.
*/
- (void)abortParsing;
@end
File diff suppressed because it is too large Load Diff
-350
View File
@@ -1,350 +0,0 @@
//
// DTHTMLElement.h
// DTCoreText
//
// Created by Oliver Drobnik on 4/14/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
@class DTCoreTextParagraphStyle;
@class DTCoreTextFontDescriptor;
@class DTTextAttachment;
@class DTCSSListStyle;
#import "DTCompatibility.h"
#import "DTCoreTextConstants.h"
#import "DTHTMLParserNode.h"
#import "DTTextAttachment.h"
@class DTBreakHTMLElement;
/**
Class to represent a element (aka "tag") in a HTML document. Structure information - like parent or children - is inherited from its superclass <DTHTMLParserNode>.
*/
@interface DTHTMLElement : DTHTMLParserNode
{
DTCoreTextFontDescriptor *_fontDescriptor;
DTCoreTextParagraphStyle *_paragraphStyle;
DTTextAttachment *_textAttachment;
DTTextAttachmentVerticalAlignment _textAttachmentAlignment;
NSURL *_link;
NSString *_anchorName;
DTColor *_textColor;
DTColor *_backgroundColor;
DTColor *_backgroundStrokeColor;
CGFloat _backgroundStrokeWidth;
CGFloat _backgroundCornerRadius;
CTUnderlineStyle _underlineStyle;
DTColor *_underlineColor;
NSString *_beforeContent;
NSString *_linkGUID;
BOOL _strikeOut;
NSInteger _superscriptStyle;
NSInteger _headerLevel;
NSArray *_shadows;
DTHTMLElementDisplayStyle _displayStyle;
DTHTMLElementFloatStyle _floatStyle;
BOOL _isColorInherited;
BOOL _preserveNewlines;
BOOL _containsAppleConvertedSpace;
DTHTMLElementFontVariant _fontVariant;
CGFloat _textScale;
CGSize _size;
NSMutableArray *_children;
NSDictionary *_styles;
BOOL _didOutput;
// margins/padding
DTEdgeInsets _margins;
DTEdgeInsets _padding;
// indent of lists
CGFloat _listIndent;
// indent of tag <p>
CGFloat _pTextIndent;
BOOL _shouldProcessCustomHTMLAttributes;
}
/**
@name Creating HTML Elements
*/
/**
Designed initializer, creates the appropriate element sub type
@param name The element name
@param attributes The attributes dictionary of the tag
@param options The parsing options dictionary
@returns the initialized element
*/
+ (DTHTMLElement *)elementWithName:(NSString *)name attributes:(NSDictionary *)attributes options:(NSDictionary *)options;
/**
@name Creating Attributed Strings
*/
/**
Creates an `NSAttributedString` that represents the receiver including all its children. This method is typically overwritten in subclasses of <DTHTMLElement> that represent specific HTML elements.
@returns An attributed string that also contains the children
*/
- (NSAttributedString *)attributedString;
/**
The dictionary of Core Text attributes for creating an `NSAttributedString` representation for the receiver
@returns The dictionary of attributes
*/
- (NSDictionary *)attributesForAttributedStringRepresentation;
/**
Creates a <DTCSSListStyle> to match the CSS styles
*/
- (DTCSSListStyle *)listStyle;
/**
@name Getting Element Information
*/
/**
Font Descriptor describing the font state of the receiver
*/
@property (nonatomic, copy) DTCoreTextFontDescriptor *fontDescriptor;
/**
Paragraph Style describing the paragraph state of the receiver
*/
@property (nonatomic, copy) DTCoreTextParagraphStyle *paragraphStyle;
/**
Text Attachment of the receiver, or `nil` if there is no attachment
*/
@property (nonatomic, strong) DTTextAttachment *textAttachment;
/**
Hyperlink URL of the receiver, or `nil` if there is no hyperlink
*/
@property (nonatomic, copy) NSURL *link;
/**
Anchor name, used by hyperlinks, of the receiver that can be used to scroll to.
*/
@property (nonatomic, copy) NSString *anchorName;
/**
Foreground text color of the receiver
*/
@property (nonatomic, strong) DTColor *textColor;
/**
Background color of text in the receiver
*/
@property (nonatomic, strong) DTColor *backgroundColor;
/**
Background stroke color in the receiver
*/
@property (nonatomic, strong) DTColor *backgroundStrokeColor;
/**
Background stroke width in the receiver
*/
@property (nonatomic, assign) CGFloat backgroundStrokeWidth;
/**
Background stroke width in the receiver
*/
@property (nonatomic, assign) CGFloat backgroundCornerRadius;
/**
Tag <p> text indent
*/
@property (nonatomic, assign) CGFloat pTextIndent;
/**
The custom letter spacing of the receiver, default is 0px
*/
@property (nonatomic, assign) CGFloat letterSpacing;
/**
Additional text to be inserted before the text content of the receiver
*/
@property (nonatomic, copy) NSString *beforeContent;
/**
Array of shadows attached to the text contents of the receiver
*/
@property (nonatomic, copy) NSArray *shadows;
/**
The underline style of the receiver, at present only none or single line are supported
*/
@property (nonatomic, assign) CTUnderlineStyle underlineStyle;
/**
The underline color of the receiver
*/
@property (nonatomic, strong) DTColor *underlineColor;
/**
The strike-out style of the receiver
*/
@property (nonatomic, assign) BOOL strikeOut;
/**
The superscript style of the receiver or 0 if it does not have superscript text.
*/
@property (nonatomic, assign) NSInteger superscriptStyle;
/**
The header level of the receiver, or 0 if it is not a header
*/
@property (nonatomic, assign) NSInteger headerLevel;
/**
The display style of the receiver.
*/
@property (nonatomic, assign) DTHTMLElementDisplayStyle displayStyle;
/**
Whether the receiver is marked as float. While floating is not currently supported this can be used to add additional paragraph breaks.
*/
@property (nonatomic, readonly) DTHTMLElementFloatStyle floatStyle;
/**
Specifies that the textColor was inherited. Assigning textColor sets this flag to `NO`
*/
@property (nonatomic, assign) BOOL isColorInherited;
/**
Specifies that whitespace and new lines should be preserved. Default is to compress white space.
*/
@property (nonatomic, assign) BOOL preserveNewlines;
/**
The current font variant of the receiver, normal or small caps.
*/
@property (nonatomic, assign) DTHTMLElementFontVariant fontVariant;
/**
The current unscaled font size (used when inheriting font size). You're probably looking for fontDescriptor.pointSize.
*/
@property (nonatomic, assign) CGFloat currentTextSize;
/**
The scale by which all fonts are scaled
*/
@property (nonatomic, assign) CGFloat textScale;
/**
The size of the receiver, either from width/height attributes or width/hight styles.
*/
@property (nonatomic, assign) CGSize size;
/**
The value of the CSS margins. Margin support is incomplete.
*/
@property (nonatomic, assign) DTEdgeInsets margins;
/** The value of the CSS padding. Padding are added to DTTextBlock instances for block-level elements.
*/
@property (nonatomic, assign) DTEdgeInsets padding;
/**
Specifies that whitespace contained in the receiver's text has been converted with Apple's algorithm.
*/
@property (nonatomic, assign) BOOL containsAppleConvertedSpace;
/**
Prevents adding custom HTML attributes to output
*/
@property (nonatomic, assign) BOOL shouldProcessCustomHTMLAttributes;
/**
@name Working with HTML Attributes
*/
/**
Retrieves an attribute with a given key
@param key The attribute name to retrieve
@returns the attribute string
*/
- (NSString *)attributeForKey:(NSString *)key;
/**
Copies and inherits relevant attributes from the given parent element
@param element The element to inherit attributes from
*/
- (void)inheritAttributesFromElement:(DTHTMLElement *)element;
/**
Interprets the tag attributes for e.g. writing direction. Usually you would call this after inheritAttributesFromElement:.
*/
- (void)interpretAttributes;
/**
The HTML attributes that should be attached to the generated attributed string. Typically all attributes that were processed by -interpretAttributes are in this list. All other attributes get added to the generated attributed string with the DTCustomAttributesAttribute key.
*/
+ (NSSet *)attributesToIgnoreForCustomAttributesAttribute;
/**
The CSS class names that are not to be added to the "class" custom attribute in the DTCustomAttributesAttribute key. Those are usually the class names
*/
@property(nonatomic, strong) NSSet *CSSClassNamesToIgnoreForCustomAttributes;
/**
@name Working with CSS Styles
*/
/**
Applies the style information contained in a styles dictionary to the receiver
@param styles A style dictionary
*/
- (void)applyStyleDictionary:(NSDictionary *)styles;
/**
@name HTML Node Hierarchy
*/
/**
Returns the parent element. That's the same as the parent node but with adjusted type for convenience.
*/
- (DTHTMLElement *)parentElement;
/**
@name Output State (Internal)
*/
/**
Internal state during string building to mark the receiver as having been flushed
*/
@property (nonatomic, assign) BOOL didOutput;
/**
Internal method that determines if this element still requires output, based on its own didOutput
state and the didOutput state of its children
@returns `YES` if it still requires output
*/
- (BOOL)needsOutput;
@end
File diff suppressed because it is too large Load Diff
@@ -1,78 +0,0 @@
//
// DTHTMLParserNode.h
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <DTFoundation/DTWeakSupport.h>
@class DTHTMLParserTextNode;
/**
This class represents one node in an HTML DOM tree.
*/
@interface DTHTMLParserNode : NSObject
{
NSDictionary *_attributes;
}
/**
Designated initializer
@param name The element name
@param attributes The attributes dictionary
@returns An initialized parser node.
*/
- (id)initWithName:(NSString *)name attributes:(NSDictionary *)attributes;
/**
The name of the receiver
*/
@property (nonatomic, copy) NSString *name;
/**
The attributes of the receiver.
*/
@property (nonatomic, copy) NSDictionary *attributes;
/**
A weak link to the parent node of the receiver
*/
@property (nonatomic, DT_WEAK_PROPERTY) DTHTMLParserNode *parentNode;
/**
The child nodes of the receiver
*/
@property (nonatomic, readonly) NSArray *childNodes;
/**
Adds a child node to the receiver.
@param childNode The child node to be appended to the list of children
*/
- (void)addChildNode:(DTHTMLParserNode *)childNode;
/**
Removes a child node from the receiver
@param childNode The child node to remove
*/
- (void)removeChildNode:(DTHTMLParserNode *)childNode;
/**
Removes all child nodes from the receiver
*/
- (void)removeAllChildNodes;
/**
Hierarchy representation of the receiver including all attributes and children
*/
- (NSString *)debugDescription;
/**
Concatenated contents of all text nodes
*/
- (NSString *)text;
@end
@@ -1,167 +0,0 @@
//
// DTHTMLParserNode.m
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHTMLParserNode.h"
#import "DTHTMLParserTextNode.h"
#import <DTFoundation/DTWeakSupport.h>
@implementation DTHTMLParserNode
{
NSString *_name;
DT_WEAK_VARIABLE DTHTMLParserNode *_parentNode;
NSMutableArray *_childNodes;
}
- (id)initWithName:(NSString *)name attributes:(NSDictionary *)attributes
{
self = [super init];
if (self)
{
_name = [name copy];
[self setAttributes:attributes]; // property to allow overriding
}
return self;
}
- (void)addChildNode:(DTHTMLParserNode *)childNode
{
@synchronized(self)
{
// first child creates array
if (!_childNodes)
{
_childNodes = [[NSMutableArray alloc] init];
}
childNode.parentNode = self;
[_childNodes addObject:childNode];
}
}
- (void)removeChildNode:(DTHTMLParserNode *)childNode
{
@synchronized(self)
{
[_childNodes removeObject:childNode];
}
}
- (void)removeAllChildNodes
{
@synchronized(self)
{
[_childNodes removeAllObjects];
}
}
#ifndef COVERAGE
// exclude methods from coverage testing
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ name='%@'>", NSStringFromClass([self class]), _name];
}
#endif
- (void)_appendHTMLToString:(NSMutableString *)string indentLevel:(NSUInteger)indentLevel
{
@synchronized(self)
{
// indent to the level
for (NSUInteger i=0; i<indentLevel; i++)
{
[string appendString:@" "];
}
// write own name tag open
[string appendFormat:@"<%@", _name];
// sort attribute names
NSArray *sortedKeys = [_attributes.allKeys sortedArrayUsingSelector:@selector(compare:)];
for (NSString *oneKey in sortedKeys)
{
NSString *attribute = [_attributes objectForKey:oneKey];
[string appendFormat:@" %@=\"%@\"", oneKey, attribute];
}
if (![_childNodes count])
{
[string appendString:@" \\>\n"];
return;
}
[string appendFormat:@">\n"];
// output attributes
for (DTHTMLParserNode *childNode in _childNodes)
{
[childNode _appendHTMLToString:string indentLevel:indentLevel+1];
}
// indent to the level
for (NSUInteger i=0; i<indentLevel; i++)
{
[string appendString:@" "];
}
// write own name tag close
[string appendFormat:@"</%@>\n", _name];
}
}
- (NSString *)debugDescription
{
@synchronized(self)
{
NSMutableString *tmpString = [NSMutableString string];
[self _appendHTMLToString:tmpString indentLevel:0];
return tmpString;
}
}
- (NSString *)text
{
@synchronized(self)
{
NSMutableString *text = [NSMutableString string];
for (DTHTMLParserTextNode *oneChild in self.childNodes)
{
if ([oneChild isKindOfClass:[DTHTMLParserTextNode class]])
{
[text appendString:[oneChild characters]];
}
}
return text;
}
}
#pragma mark - Properties
- (NSArray *)childNodes
{
@synchronized(self)
{
return _childNodes;
}
}
@synthesize name = _name;
@synthesize attributes = _attributes;
@synthesize parentNode = _parentNode;
@synthesize childNodes = _childNodes;
@end
@@ -1,30 +0,0 @@
//
// DTHTMLParserTextNode.h
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "DTHTMLParserNode.h"
/**
Specialized sub class of <DTHTMLParserNode> that represents text inside a node
*/
@interface DTHTMLParserTextNode : DTHTMLParserNode
/**
Designated initializer with the characters that make up the text.
@param characters The characters of the string
@returns The initialized text node
*/
- (id)initWithCharacters:(NSString *)characters;
/**
Returns the receivers character contents
*/
@property (nonatomic, readonly) NSString *characters;
@end
@@ -1,56 +0,0 @@
//
// DTHTMLParserTextNode.m
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHTMLParserTextNode.h"
#import "NSString+HTML.h"
@implementation DTHTMLParserTextNode
{
NSString *_characters;
}
- (id)initWithCharacters:(NSString *)characters
{
self = [super init];
if (self)
{
self.name = @"#TEXT#";
_characters = characters;
}
return self;
}
#ifndef COVERAGE
// exclude methods from coverage testing
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ content='%@'>", NSStringFromClass([self class]), _characters];
}
- (void)_appendHTMLToString:(NSMutableString *)string indentLevel:(NSUInteger)indentLevel
{
// indent to the level
for (NSUInteger i=0; i<indentLevel; i++)
{
[string appendString:@" "];
}
[string appendFormat:@"\"%@\"\n", [_characters stringByNormalizingWhitespace]];
}
#endif
#pragma mark - Properties
@synthesize characters = _characters;
@end
-69
View File
@@ -1,69 +0,0 @@
//
// DTHTMLWriter.h
// DTCoreText
//
// Created by Oliver Drobnik on 23.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
/**
Class to generate HTML from `NSAttributedString` instances.
*/
@interface DTHTMLWriter : NSObject
/**
@name Creating an HTML Writer
*/
/**
Creates a writer with a given `NSAttributedString` as input
@param attributedString An attributed string
*/
- (id)initWithAttributedString:(NSAttributedString *)attributedString;
/**
@name Generating HTML
*/
/**
Generates a HTML representation of the attributed string
@returns The generated string
*/
- (NSString *)HTMLString;
/**
Generates a HTML fragment representation of the attributed string including inlined styles and no html or head elements
@returns The generated string
*/
- (NSString *)HTMLFragment;
/**
@name Properties
*/
/**
If specified then all absolute font sizes (px) will be divided by this value. This is useful if you specified a text size multiplier when converting HTML to the attributed string you are processing.
*/
@property (nonatomic, assign) CGFloat textScale;
/**
If YES, preserve whitespaces in HTML by using "Apple-converted-space". Default YES.
*/
@property (nonatomic, assign) BOOL useAppleConvertedSpace;
/**
The attributed string that the writer is processing.
*/
@property (nonatomic, readonly) NSAttributedString *attributedString;
/**
The HTML element tag name to use for paragraphs. Defaults to @"p".
*/
@property (nonatomic, strong) NSString *paragraphTagName;
@end
File diff suppressed because it is too large Load Diff
@@ -1,17 +0,0 @@
//
// DTHTMLElementHR.h
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHTMLElement.h"
/**
Specialized subclass of <DTHTMLElement> that deals with list items.
*/
@interface DTHorizontalRuleHTMLElement : DTHTMLElement
@end
@@ -1,30 +0,0 @@
//
// DTHTMLElementHR.m
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHorizontalRuleHTMLElement.h"
@implementation DTHorizontalRuleHTMLElement
- (NSDictionary *)attributesForAttributedStringRepresentation
{
NSMutableDictionary *dict = [[super attributesForAttributedStringRepresentation] mutableCopy];
[dict setObject:[NSNumber numberWithBool:YES] forKey:DTHorizontalRuleStyleAttribute];
return dict;
}
- (NSAttributedString *)attributedString
{
@synchronized(self)
{
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
return [[NSAttributedString alloc] initWithString:@"\n" attributes:attributes];
}
}
@end
@@ -1,17 +0,0 @@
//
// DTTextAttachmentIframe.h
// DTCoreText
//
// Created by Oliver Drobnik on 22.04.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTTextAttachment.h"
/**
A specialized subclass in the DTTextAttachment class cluster to represent an IFRAME
*/
@interface DTIframeTextAttachment : DTTextAttachment <DTTextAttachmentHTMLPersistence>
@end
@@ -1,115 +0,0 @@
//
// DTTextAttachmentIframe.m
// DTCoreText
//
// Created by Oliver Drobnik on 22.04.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTIframeTextAttachment.h"
#import "DTCoreTextConstants.h"
#import "DTHTMLElement.h"
#import "NSString+HTML.h"
@implementation DTIframeTextAttachment
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
{
self = [super initWithElement:element options:options];
if (self)
{
// get base URL
NSURL *baseURL = [options objectForKey:NSBaseURLDocumentOption];
NSString *src = [element.attributes objectForKey:@"src"];
// prepend http: if URL string starts with // (seems to do with youtube iframes as standard)
if ([src hasPrefix:@"//"]) {
src = [@"http:" stringByAppendingString:src];
}
// content URL
_contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
}
return self;
}
#pragma mark - DTTextAttachmentHTMLEncoding
- (NSString *)stringByEncodingAsHTML
{
NSMutableString *retString = [NSMutableString string];
[retString appendString:@"<iframe"];
if (_contentURL)
{
[retString appendFormat:@" src=\"%@\"", [_contentURL absoluteString]];
}
// build style for img/video
NSMutableString *styleString = [NSMutableString string];
switch (_verticalAlignment)
{
case DTTextAttachmentVerticalAlignmentBaseline:
{
// [classStyleString appendString:@"vertical-align:baseline;"];
break;
}
case DTTextAttachmentVerticalAlignmentTop:
{
[styleString appendString:@"vertical-align:text-top;"];
break;
}
case DTTextAttachmentVerticalAlignmentCenter:
{
[styleString appendString:@"vertical-align:middle;"];
break;
}
case DTTextAttachmentVerticalAlignmentBottom:
{
[styleString appendString:@"vertical-align:text-bottom;"];
break;
}
}
if (_originalSize.width>0)
{
[styleString appendFormat:@"width:%.0fpx;", _originalSize.width];
}
if (_originalSize.height>0)
{
[styleString appendFormat:@"height:%.0fpx;", _originalSize.height];
}
// add local style for size, since sizes might vary quite a bit
if ([styleString length])
{
[retString appendFormat:@" style=\"%@\"", styleString];
}
// attach the attributes dictionary
NSMutableDictionary *tmpAttributes = [_attributes mutableCopy];
// remove src,style, width and height we already have these
[tmpAttributes removeObjectForKey:@"src"];
[tmpAttributes removeObjectForKey:@"style"];
[tmpAttributes removeObjectForKey:@"width"];
[tmpAttributes removeObjectForKey:@"height"];
for (__strong NSString *oneKey in [tmpAttributes allKeys])
{
oneKey = [oneKey stringByAddingHTMLEntities];
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
}
[retString appendString:@" />"];
return retString;
}
@end
-49
View File
@@ -1,49 +0,0 @@
//
// DTImage+HTML.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/9/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE
#import <UIKit/UIImage.h>
/**
Category used to have the same method available for unit testing on Mac on iOS.
*/
@interface UIImage (HTML)
/**
Retrieve the NSData representation of a UIImage. Used to encode UIImages in DTTextAttachments.
@returns The NSData representation of the UIImage instance receiving this message. Convenience method for UIImagePNGRepresentation().
*/
- (NSData *)dataForPNGRepresentation;
@end
#endif
#if TARGET_OS_OSX
#import <AppKit/NSImage.h>
/**
Category used to have the same method available for unit testing on Mac on iOS.
*/
@interface NSImage (HTML)
/**
Retrieve the NSData representation of a NSImage.
@returns The NSData representation of the NSImage instance receiving this message.
*/
- (NSData *)dataForPNGRepresentation;
@end
#endif
-39
View File
@@ -1,39 +0,0 @@
//
// DTImage+HTML.m
// DTCoreText
//
// Created by Oliver Drobnik on 31.01.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTImage+HTML.h"
#if TARGET_OS_IPHONE
@implementation UIImage (HTML)
- (NSData *)dataForPNGRepresentation
{
return UIImagePNGRepresentation(self);
}
@end
#endif
#if TARGET_OS_OSX
@implementation NSImage (HTML)
- (NSData *)dataForPNGRepresentation
{
[self lockFocus];
NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0, self.size.width, self.size.height)];
[self unlockFocus];
return [bitmapRep representationUsingType:NSPNGFileType properties:[NSDictionary new]];
}
@end
#endif
@@ -1,42 +0,0 @@
//
// DTTextAttachmentImage.h
// DTCoreText
//
// Created by Oliver Drobnik on 22.04.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#import "DTTextAttachment.h"
@class DTImage;
/**
A specialized subclass in the DTTextAttachment class cluster to represent an embedded image
*/
@interface DTImageTextAttachment : DTTextAttachment <DTTextAttachmentDrawing, DTTextAttachmentHTMLPersistence>
/**
The designated initializer which will be called by [DTTextAttachment textAttachmentWithElement:options:] for image attachments.
@param element A DTHTMLElement that must have a valid tag name and should have a size. Any element attributes are copied to the text attachment's elements.
@param options An NSDictionary of options. Used to specify the max image size with the key DTMaxImageSize.
*/
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options;
/**
@name Alternate Representations
*/
/**
Retrieves a string which is in the format "data:image/png;base64,%@" with this DTTextAttachment's content's data representation encoded in Base64 string encoding. For image contents only.
@returns A Base64 encoded string of the png data representation of this text attachment's image contents.
*/
- (NSString *)dataURLRepresentation;
/**
The image represented by the receiver
*/
@property (nonatomic, strong) DTImage *image;
@end
@@ -1,501 +0,0 @@
//
// DTImageTextAttachment.m
// DTCoreText
//
// Created by Oliver Drobnik on 22.04.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTImageTextAttachment.h"
#import "DTCoreTextConstants.h"
#import "DTHTMLElement.h"
#import "NSString+CSS.h"
#import "NSString+HTML.h"
#import "DTImage+HTML.h"
#if TARGET_OS_IPHONE
#import <DTFoundation/DTAnimatedGIF.h>
#endif
#import <DTFoundation/DTBase64Coding.h>
static NSCache *imageCache = nil;
@interface DTImageTextAttachment () // private stuff
+ (NSCache *)sharedImageCache;
@end
@implementation DTImageTextAttachment
{
DTImage *_image;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_image = [aDecoder decodeObjectForKey:@"image"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeObject:_image forKey:@"image"];
}
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
{
self = [super initWithElement:element options:options];
if (self)
{
[self _decodeImageFromElement:element options:options];
}
return self;
}
- (id)initWithImage:(DTImage *)image
{
self = [super init];
if (self)
{
self.image = image;
}
return self;
}
- (void)_decodeImageFromElement:(DTHTMLElement *)element options:(NSDictionary *)options
{
// get base URL
NSURL *baseURL = [options objectForKey:NSBaseURLDocumentOption];
NSString *src = [element.attributes objectForKey:@"src"];
NSURL *contentURL = nil;
// decode content URL
if ([src length]) // guard against img with no src
{
if ([src hasPrefix:@"data:"])
{
NSString *cleanStr = [[src componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsJoinedByString:@""];
NSURL *dataURL = [NSURL URLWithString:cleanStr];
// try native decoding first
NSData *decodedData = [NSData dataWithContentsOfURL:dataURL];
// try own base64 decoding
if (!decodedData)
{
NSRange range = [cleanStr rangeOfString:@"base64,"];
if (range.length)
{
NSString *encodedData = [cleanStr substringFromIndex:range.location + range.length];
decodedData = [DTBase64Coding dataByDecodingString:encodedData];
}
}
// if we have image data, get the default display size
if (decodedData)
{
DTImage *decodedImage = [[DTImage alloc] initWithData:decodedData];
// we don't know the content scale from such images, need to infer it from size in style
NSString *styles = [element.attributes objectForKey:@"style"];
// that only works if there is a style dictionary
if (styles)
{
NSDictionary *attributes = [styles dictionaryOfCSSStyles];
NSString *widthStr = attributes[@"width"];
NSString *heightStr = attributes[@"height"];
if ([widthStr hasSuffix:@"px"] && [heightStr hasSuffix:@"px"])
{
CGSize sizeAccordingToStyle;
// those style size values are the original image size
sizeAccordingToStyle.width = [widthStr pixelSizeOfCSSMeasureRelativeToCurrentTextSize:0 textScale:1];
sizeAccordingToStyle.height = [heightStr pixelSizeOfCSSMeasureRelativeToCurrentTextSize:0 textScale:1];
// if _orgiginal width and height are a fraction of decode image size, it must be a scaled image
if (sizeAccordingToStyle.width != 0 && sizeAccordingToStyle.width < decodedImage.size.width &&
sizeAccordingToStyle.height != 0 && sizeAccordingToStyle.height < decodedImage.size.height)
{
// determine image scale
CGFloat scale = round(decodedImage.size.width/sizeAccordingToStyle.width);
// sanity check, accept from @2x - @5x
if (scale>=2.0 && scale<=5.0)
{
#if TARGET_OS_IPHONE
// on iOS change the scale by making a new image with same pixels
decodedImage = [DTImage imageWithCGImage:decodedImage.CGImage scale:scale orientation:decodedImage.imageOrientation];
#else
// on OS X we can set the size
[decodedImage setSize:sizeAccordingToStyle];
#endif
}
}
}
}
self.image = decodedImage;
// prevent remote loading of image
_contentURL = nil;
}
}
else // normal URL
{
contentURL = [NSURL URLWithString:src];
if (!contentURL)
{
src = [src stringByAddingHTMLEntities];
contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
}
if (!contentURL)
{
src = [src stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
contentURL = [NSURL URLWithString:src];
}
if (![contentURL scheme])
{
// possibly a relative url
if (baseURL)
{
contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
}
else
{
// file in app bundle
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *path = [bundle pathForResource:src ofType:nil];
if (path)
{
// Prevent a crash if path turns up nil.
contentURL = [NSURL fileURLWithPath:path];
}
else
{
// might also be in a different bundle, e.g. when unit testing
bundle = [NSBundle bundleForClass:[DTTextAttachment class]];
path = [bundle pathForResource:src ofType:nil];
if (path)
{
// Prevent a crash if path turns up nil.
contentURL = [NSURL fileURLWithPath:path];
}
}
}
}
}
}
// if it's a local file we need to inspect it to get it's dimensions
if (_displaySize.width==0 || _displaySize.height==0)
{
DTImage *image = _image;
// let's check if we have a cached image already then we can inspect that
if (!_image)
{
image = [[DTImageTextAttachment sharedImageCache] objectForKey:[contentURL absoluteString]];
}
if (!image)
{
// only local files we can directly load without punishment
if ([contentURL isFileURL])
{
#if TARGET_OS_IPHONE
NSString *ext = [[[contentURL lastPathComponent] pathExtension] lowercaseString];
if ([ext isEqualToString:@"gif"])
{
image = DTAnimatedGIFFromFile([contentURL path]);
}
else
#endif
{
image = [[DTImage alloc] initWithContentsOfFile:[contentURL path]];
}
}
// cache that for later
if (image)
{
[[DTImageTextAttachment sharedImageCache] setObject:image forKey:[contentURL absoluteString]];
}
}
// we have an image, so we can set the original size and default display size
if (image)
{
_contentURL = nil;
[self _updateSizesFromImage:image];
}
}
// only remote images should have a URL
_contentURL = contentURL;
}
- (void)_updateSizesFromImage:(DTImage *)image
{
// set original size if there is none set yet
if (CGSizeEqualToSize(_originalSize, CGSizeZero))
{
_originalSize = image.size;
}
else
{
// get the other dimension if one is missing
if (_originalSize.width==0 && _originalSize.height!=0)
{
CGFloat factor = _originalSize.height/image.size.height;
_originalSize.width = image.size.width * factor;
}
else if (_originalSize.width!=0 && _originalSize.height==0)
{
CGFloat factor = _originalSize.width/image.size.width;
_originalSize.height = image.size.height * factor;
}
}
// initial display size matches original
if (CGSizeEqualToSize(CGSizeZero, _displaySize))
{
[self setDisplaySize:_originalSize withMaxDisplaySize:_maxImageSize];
}
else
{
// get the other dimension if one is missing
if (_displaySize.width==0 && _displaySize.height!=0)
{
CGSize newDisplaySize = _displaySize;
CGFloat factor = _displaySize.height/_originalSize.height;
newDisplaySize.width = _originalSize.width * factor;
[self setDisplaySize:newDisplaySize withMaxDisplaySize:_maxImageSize];
}
else if (_displaySize.width!=0 && _displaySize.height==0)
{
CGSize newDisplaySize = _displaySize;
CGFloat factor = _displaySize.width/_originalSize.width;
newDisplaySize.height = _originalSize.height * factor;
[self setDisplaySize:newDisplaySize withMaxDisplaySize:_maxImageSize];
}
}
}
+ (NSCache *)sharedImageCache {
if (imageCache) return imageCache;
static dispatch_once_t onceToken; // lock
dispatch_once(&onceToken, ^{ // this block run only once
imageCache = [[NSCache alloc] init];
});
return imageCache;
}
#pragma mark - Alternative Representations
// makes a data URL of the image
- (NSString *)dataURLRepresentation
{
DTImage *image = self.image;
if (!image)
{
return nil;
}
NSData *data = [image dataForPNGRepresentation];
NSString *encoded = [DTBase64Coding stringByEncodingData:data];
return [@"data:image/png;base64," stringByAppendingString:encoded];
}
#pragma mark - DTTextAttachmentDrawing
- (void)drawInRect:(CGRect)rect context:(CGContextRef)context
{
[self.image drawInRect:rect];
}
#pragma mark - DTTextAttachmentHTMLEncoding
- (NSString *)stringByEncodingAsHTML
{
NSMutableString *retString = [NSMutableString string];
NSString *urlString;
if (_contentURL)
{
if ([_contentURL isFileURL])
{
NSString *path = [_contentURL path];
NSRange range = [path rangeOfString:@".app/"];
if (range.length)
{
urlString = [path substringFromIndex:NSMaxRange(range)];
}
else
{
urlString = [_contentURL absoluteString];
}
}
else
{
urlString = [_contentURL relativeString];
}
}
else
{
urlString = [self dataURLRepresentation];
}
// output tag start
[retString appendString:@"<img"];
// build style for img/video
NSMutableString *styleString = [NSMutableString string];
switch (_verticalAlignment)
{
case DTTextAttachmentVerticalAlignmentBaseline:
{
// [classStyleString appendString:@"vertical-align:baseline;"];
break;
}
case DTTextAttachmentVerticalAlignmentTop:
{
[styleString appendString:@"vertical-align:text-top;"];
break;
}
case DTTextAttachmentVerticalAlignmentCenter:
{
[styleString appendString:@"vertical-align:middle;"];
break;
}
case DTTextAttachmentVerticalAlignmentBottom:
{
[styleString appendString:@"vertical-align:text-bottom;"];
break;
}
}
if (_originalSize.width>0)
{
[styleString appendFormat:@"width:%.0fpx;", _originalSize.width];
}
if (_originalSize.height>0)
{
[styleString appendFormat:@"height:%.0fpx;", _originalSize.height];
}
// add local style for size, since sizes might vary quite a bit
if ([styleString length])
{
[retString appendFormat:@" style=\"%@\"", styleString];
}
[retString appendFormat:@" src=\"%@\"", urlString];
// attach the attributes dictionary
NSMutableDictionary *tmpAttributes = [_attributes mutableCopy];
// remove src,style, width and height we already have these
[tmpAttributes removeObjectForKey:@"src"];
[tmpAttributes removeObjectForKey:@"style"];
[tmpAttributes removeObjectForKey:@"width"];
[tmpAttributes removeObjectForKey:@"height"];
for (__strong NSString *oneKey in [tmpAttributes allKeys])
{
oneKey = [oneKey stringByAddingHTMLEntities];
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
}
// end
[retString appendString:@" />"];
return retString;
}
#pragma mark - Properties
/**
Accessor for the contents instance variable. If the content type is DTTextAttachmentTypeImage this returns a DTImage instance of the contents.
@returns Contents. If it is an image, a DTImage instance is returned. Otherwise it is returned as is.
*/
- (DTImage *)image
{
if (!_image)
{
if (_contentURL)
{
DTImage *image = [[DTImageTextAttachment sharedImageCache] objectForKey:[_contentURL absoluteString]];
// only local files can be loaded into cache
if (!image && [_contentURL isFileURL])
{
image = [[DTImage alloc] initWithContentsOfFile:[_contentURL path]];
// cache it
if (image)
{
[[DTImageTextAttachment sharedImageCache] setObject:image forKey:[_contentURL absoluteString]];
}
}
return image;
}
}
return _image;
}
- (void)setImage:(DTImage *)image
{
if (_image != image)
{
_image = image;
[self _updateSizesFromImage:_image];
}
}
- (void)setDisplaySize:(CGSize)displaySize
{
_displaySize = displaySize;
}
@end
@@ -1,87 +0,0 @@
//
// DTLazyImageView.h
// DTCoreText
//
// Created by Oliver Drobnik on 5/20/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <DTFoundation/DTWeakSupport.h>
#import "DTAttributedTextContentView.h"
@class DTLazyImageView;
// Notifications
extern NSString * const DTLazyImageViewWillStartDownloadNotification;
extern NSString * const DTLazyImageViewDidFinishDownloadNotification;
/**
Protocol for delegates of <DTLazyImageView> to inform them about the downloaded image dimensions.
*/
@protocol DTLazyImageViewDelegate <NSObject>
@optional
/**
Method that informs the delegate about the image size so that it can re-layout text.
@param lazyImageView The image view
@param size The image size that is now known
*/
- (void)lazyImageView:(DTLazyImageView *)lazyImageView didChangeImageSize:(CGSize)size;
@end
/**
This `UIImageView` subclass lazily loads an image from a URL and informs a delegate once the size of the image is known.
*/
@interface DTLazyImageView : UIImageView
/**
@name Providing Content
*/
/**
The URL of the remote image
*/
@property (nonatomic, strong) NSURL *url;
/**
The URL Request that is to be used for downloading the image. If this is left `nil` the a new URL Request will be created
*/
@property (nonatomic, strong) NSMutableURLRequest *urlRequest;
/**
The DTAttributedTextContentView used to display remote images with DTAttributedTextCell
*/
@property (nonatomic, DT_WEAK_PROPERTY) DTAttributedTextContentView *contentView;
/**
@name Getting Information
*/
/**
Set to `YES` to support progressive display of progressive downloads
*/
@property (nonatomic, assign) BOOL shouldShowProgressiveDownload;
/**
The delegate, conforming to <DTLazyImageViewDelegate>, to inform when the image dimensions were determined
*/
@property (nonatomic, DT_WEAK_PROPERTY) id<DTLazyImageViewDelegate> delegate;
/**
@name Cancelling Download
*/
/**
Cancels the image downloading
*/
- (void)cancelLoading;
@end
#endif
-420
View File
@@ -1,420 +0,0 @@
//
// DTLazyImageView.m
// DTCoreText
//
// Created by Oliver Drobnik on 5/20/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTLazyImageView.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <ImageIO/ImageIO.h>
#import <DTFoundation/DTLog.h>
static NSCache *_imageCache = nil;
NSString * const DTLazyImageViewWillStartDownloadNotification = @"DTLazyImageViewWillStartDownloadNotification";
NSString * const DTLazyImageViewDidFinishDownloadNotification = @"DTLazyImageViewDidFinishDownloadNotification";
@interface DTLazyImageView ()
#if DTCORETEXT_USES_NSURLSESSION
<NSURLSessionDataDelegate>
#else
<NSURLConnectionDelegate>
#endif
- (void)_notifyDelegate;
@end
@implementation DTLazyImageView
{
NSURL *_url;
NSMutableURLRequest *_urlRequest;
#if DTCORETEXT_USES_NSURLSESSION
NSURLSessionDataTask *_dataTask;
NSURLSession *_session;
#else
NSURLConnection *_connection;
#endif
NSMutableData *_receivedData;
/* For progressive download */
CGImageSourceRef _imageSource;
CGFloat _fullHeight;
CGFloat _fullWidth;
NSUInteger _expectedSize;
BOOL shouldShowProgressiveDownload;
DT_WEAK_VARIABLE id<DTLazyImageViewDelegate> _delegate;
}
- (void)dealloc
{
_delegate = nil; // to avoid late notification
#if DTCORETEXT_USES_NSURLSESSION
[_dataTask cancel];
#else
[_connection cancel];
#endif
if (_imageSource) CFRelease(_imageSource);
}
- (void)loadImageAtURL:(NSURL *)url
{
// local files we don't need to get asynchronously
if ([url isFileURL] || [url.scheme isEqualToString:@"data"])
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
[self completeDownloadWithData:data];
});
});
return;
}
@autoreleasepool
{
if (!_urlRequest)
{
_urlRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
}
else
{
[_urlRequest setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[_urlRequest setTimeoutInterval:10.0];
}
[[NSNotificationCenter defaultCenter] postNotificationName:DTLazyImageViewWillStartDownloadNotification object:self];
#if DTCORETEXT_USES_NSURLSESSION
if (!_session)
{
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
}
_dataTask = [_session dataTaskWithRequest:_urlRequest];
[_dataTask resume];
#else
_connection = [[NSURLConnection alloc] initWithRequest:_urlRequest delegate:self startImmediately:NO];
[_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[_connection start];
#endif
}
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if (!self.image && (_url || _urlRequest) &&
#if DTCORETEXT_USES_NSURLSESSION
!_dataTask
#else
!_connection
#endif
&& self.superview)
{
UIImage *image = [_imageCache objectForKey:_url];
if (image)
{
self.image = image;
_fullWidth = image.size.width;
_fullHeight = image.size.height;
// this has to be synchronous
[self _notifyDelegate];
return;
}
[self loadImageAtURL:_url];
}
}
- (void)cancelLoading
{
#if DTCORETEXT_USES_NSURLSESSION
[_dataTask cancel];
_dataTask = nil;
#else
[_connection cancel];
_connection = nil;
#endif
_receivedData = nil;
}
#pragma mark Progressive Image
-(CGImageRef)newTransitoryImage:(CGImageRef)partialImg
{
const size_t height = CGImageGetHeight(partialImg);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
size_t lFullWidth = (size_t)ceil(_fullWidth);
size_t lFullHeight = (size_t)ceil(_fullHeight);
CGContextRef bmContext = CGBitmapContextCreate(NULL, lFullWidth, lFullHeight, 8, lFullWidth * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (!bmContext)
{
// fail creating context
return NULL;
}
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = _fullWidth, .size.height = height}, partialImg);
CGImageRef goodImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
return goodImageRef;
}
- (void)createAndShowProgressiveImage
{
if (!_imageSource)
{
return;
}
/* For progressive download */
const NSUInteger totalSize = [_receivedData length];
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)_receivedData, (totalSize == _expectedSize) ? true : false);
if (_fullHeight > 0 && _fullWidth > 0)
{
CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
if (image)
{
CGImageRef imgTmp = [self newTransitoryImage:image]; // iOS fix to correctly handle JPG see : http://www.cocoabyss.com/mac-os-x/progressive-image-download-imageio/
if (imgTmp)
{
UIImage *uimage = [[UIImage alloc] initWithCGImage:imgTmp];
CGImageRelease(imgTmp);
dispatch_async(dispatch_get_main_queue(), ^{ self.image = uimage; } );
}
CGImageRelease(image);
}
}
else
{
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
if (properties)
{
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val)
CFNumberGetValue(val, kCFNumberFloatType, &_fullHeight);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val)
CFNumberGetValue(val, kCFNumberFloatType, &_fullWidth);
CFRelease(properties);
}
}
}
#pragma mark NSURL Loading
- (void)_notifyDelegate
{
if ([self.delegate respondsToSelector:@selector(lazyImageView:didChangeImageSize:)]) {
[self.delegate lazyImageView:self didChangeImageSize:CGSizeMake(_fullWidth, _fullHeight)];
}
}
- (void)completeDownloadWithData:(NSData *)data
{
UIImage *image = [[UIImage alloc] initWithData:data];
self.image = image;
_fullWidth = image.size.width;
_fullHeight = image.size.height;
[self _notifyDelegate];
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
_imageCache = [[NSCache alloc] init];
});
if (_url)
{
if (image)
{
// cache image
[_imageCache setObject:image forKey:_url];
}
else
{
DTLogWarning(@"Warning, %@ did not get an image for %@", NSStringFromClass([self class]), [_url absoluteString]);
}
}
}
#if DTCORETEXT_USES_NSURLSESSION
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
#else
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
#endif
{
// every time we get an response it might be a forward, so we discard what data we have
_receivedData = nil;
// does not fire for local file URLs
if ([response isKindOfClass:[NSHTTPURLResponse class]])
{
NSHTTPURLResponse *httpResponse = (id)response;
if (![[httpResponse MIMEType] hasPrefix:@"image"])
{
#if DTCORETEXT_USES_NSURLSESSION
completionHandler(NSURLSessionResponseCancel);
#else
[self cancelLoading];
#endif
return;
}
#if DTCORETEXT_USES_NSURLSESSION
completionHandler(NSURLSessionResponseAllow);
#endif
}
/* For progressive download */
_fullWidth = _fullHeight = -1.0f;
_expectedSize = (NSUInteger)[response expectedContentLength];
_receivedData = [[NSMutableData alloc] init];
}
#if DTCORETEXT_USES_NSURLSESSION
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
#else
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
#endif
{
[_receivedData appendData:data];
if (!&CGImageSourceCreateIncremental || !shouldShowProgressiveDownload)
{
// don't show progressive
return;
}
if (!_imageSource)
{
_imageSource = CGImageSourceCreateIncremental(NULL);
}
[self createAndShowProgressiveImage];
}
- (void)removeFromSuperview
{
[super removeFromSuperview];
#if DTCORETEXT_USES_NSURLSESSION
[_dataTask cancel];
[_session invalidateAndCancel];
#else
[_connection cancel];
#endif
}
#if DTCORETEXT_USES_NSURLSESSION
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
#else
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
#endif
{
#if DTCORETEXT_USES_NSURLSESSION
if (error)
{
[self connection:nil didFailWithError:error];
return;
}
#endif
if (_receivedData)
{
[self performSelectorOnMainThread:@selector(completeDownloadWithData:) withObject:_receivedData waitUntilDone:YES];
_receivedData = nil;
}
#if DTCORETEXT_USES_NSURLSESSION
[_session finishTasksAndInvalidate];
_dataTask = nil;
#else
_connection = nil;
#endif
/* For progressive download */
if (_imageSource)
{
CFRelease(_imageSource);
_imageSource = NULL;
}
CFRunLoopStop(CFRunLoopGetCurrent());
// success = no userInfo
[[NSNotificationCenter defaultCenter] postNotificationName:DTLazyImageViewDidFinishDownloadNotification object:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
#if DTCORETEXT_USES_NSURLSESSION
[_session invalidateAndCancel];
_dataTask = nil;
#else
_connection = nil;
#endif
_receivedData = nil;
/* For progressive download */
if (_imageSource)
{
CFRelease(_imageSource);
_imageSource = NULL;
}
CFRunLoopStop(CFRunLoopGetCurrent());
// send completion notification, pack in error as well
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:error forKey:@"Error"];
[[NSNotificationCenter defaultCenter] postNotificationName:DTLazyImageViewDidFinishDownloadNotification object:self userInfo:userInfo];
}
#pragma mark Properties
- (void) setUrlRequest:(NSMutableURLRequest *)request
{
_urlRequest = request;
self.url = [_urlRequest URL];
}
@synthesize delegate=_delegate;
@synthesize shouldShowProgressiveDownload;
@synthesize url = _url;
@synthesize urlRequest = _urlRequest;
@end
#endif
-53
View File
@@ -1,53 +0,0 @@
//
// DTLinkButton.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/16/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import <UIKit/UIKit.h>
/**
Constant for highlighting notification
*/
extern NSString *DTLinkButtonDidHighlightNotification;
/**
A button that corresponds to a hyperlink.
Multiple parts of the same hyperlink synchronize their looks through the guid. You can show link text in a different color for normal and highlighted mode by setting the button images for these states.
*/
@interface DTLinkButton : UIButton
/**
The URL that this button corresponds to.
*/
@property (nonatomic, copy) NSURL *URL;
/**
The unique identifier (GUID) that all parts of the same hyperlink have in common.
*/
@property (nonatomic, copy) NSString *GUID;
/**
The minimum size that the receiver should respond on hits with. Adjusts the bounds if they are smaller than the passed size.
*/
@property (nonatomic, assign) CGSize minimumHitSize;
/**
A Boolean value that determines whether tapping the button causes it to show a gray rounded rectangle. Default is YES.
*/
@property(nonatomic) BOOL showsTouchWhenHighlighted;
@end
#endif
-185
View File
@@ -1,185 +0,0 @@
//
// DTLinkButton.m
// DTCoreText
//
// Created by Oliver Drobnik on 1/16/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTLinkButton.h"
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
#import "DTCoreText.h"
// constant for notification
NSString *DTLinkButtonDidHighlightNotification = @"DTLinkButtonDidHighlightNotification";
@interface DTLinkButton ()
- (void)highlightNotification:(NSNotification *)notification;
@end
@implementation DTLinkButton
{
NSURL *_URL;
NSString *_GUID;
CGSize _minimumHitSize;
BOOL _showsTouchWhenHighlighted;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.userInteractionEnabled = YES;
self.enabled = YES;
self.opaque = NO;
_showsTouchWhenHighlighted = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(highlightNotification:) name:DTLinkButtonDidHighlightNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark Drawing the Run
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
if (self.highlighted)
{
if (_showsTouchWhenHighlighted)
{
CGRect imageRect = [self contentRectForBounds:self.bounds];
UIBezierPath *roundedPath = [UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:3.0f];
CGContextSetGrayFillColor(ctx, 0.73f, 0.4f);
[roundedPath fill];
}
}
}
#pragma mark Utilitiy
- (void)_adjustBoundsIfNecessary
{
CGRect bounds = self.bounds;
CGFloat widthExtend = 0;
CGFloat heightExtend = 0;
if (bounds.size.width < _minimumHitSize.width)
{
widthExtend = _minimumHitSize.width - bounds.size.width;
}
if (bounds.size.height < _minimumHitSize.height)
{
heightExtend = _minimumHitSize.height - bounds.size.height;
}
if (widthExtend>0 || heightExtend>0)
{
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(ceil(heightExtend/2.0f), ceil(widthExtend/2.0f), ceil(heightExtend/2.0f), ceil(widthExtend/2.0f));
// extend bounds by the calculated necessary edge insets
bounds.size.width += edgeInsets.left + edgeInsets.right;
bounds.size.height += edgeInsets.top + edgeInsets.bottom;
// apply bounds and insets
self.bounds = bounds;
self.contentEdgeInsets = edgeInsets;
}
else
{
self.contentEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 0);
}
}
#pragma mark Notifications
- (void)highlightNotification:(NSNotification *)notification
{
if ([notification object] == self)
{
// that was me
return;
}
NSDictionary *userInfo = [notification userInfo];
NSString *guid = [userInfo objectForKey:@"GUID"];
if ([guid isEqualToString:_GUID])
{
BOOL highlighted = [[userInfo objectForKey:@"Highlighted"] boolValue];
[super setHighlighted:highlighted];
[self setNeedsDisplay];
}
}
#pragma mark Properties
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
[self setNeedsDisplay];
// notify other parts of the same link
if (_GUID)
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:highlighted], @"Highlighted", _GUID, @"GUID", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:DTLinkButtonDidHighlightNotification object:self userInfo:userInfo];
}
}
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
if (CGRectIsEmpty(frame))
{
return;
}
[self _adjustBoundsIfNecessary];
}
- (void)setMinimumHitSize:(CGSize)minimumHitSize
{
if (CGSizeEqualToSize(_minimumHitSize, minimumHitSize))
{
return;
}
_minimumHitSize = minimumHitSize;
[self _adjustBoundsIfNecessary];
}
@synthesize URL = _URL;
@synthesize GUID = _GUID;
@synthesize minimumHitSize = _minimumHitSize;
@synthesize showsTouchWhenHighlighted = _showsTouchWhenHighlighted;
@end
#endif
@@ -1,16 +0,0 @@
//
// DTHTMLElementLI.h
// DTCoreText
//
// Created by Oliver Drobnik on 27.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHTMLElement.h"
/**
Specialized subclass of <DTHTMLElement> that deals with list items.
*/
@interface DTListItemHTMLElement : DTHTMLElement
@end
@@ -1,339 +0,0 @@
//
// DTHTMLElementLI.m
// DTCoreText
//
// Created by Oliver Drobnik on 27.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTListItemHTMLElement.h"
#import "DTCompatibility.h"
#import "DTCoreTextParagraphStyle.h"
#import "DTCoreTextFontDescriptor.h"
#import "NSDictionary+DTCoreText.h"
#import "DTCSSListStyle.h"
#import "NSString+CSS.h"
#import "DTImageTextAttachment.h"
#import "NSMutableAttributedString+HTML.h"
#import "NSAttributedStringRunDelegates.h"
#if TARGET_OS_IPHONE
#import "UIFont+DTCoreText.h"
#endif
@implementation DTListItemHTMLElement
- (NSUInteger)_indexOfListItemInListRoot:(DTHTMLElement *)listRoot
{
@synchronized(self)
{
NSInteger index = -1;
NSArray *childNodes = [listRoot.childNodes copy];
for (DTHTMLElement *oneElement in childNodes)
{
if ([oneElement isKindOfClass:[DTListItemHTMLElement class]])
{
index++;
}
if (oneElement == self)
{
break;
}
}
return index;
}
}
// calculates the accumulated list indent
- (CGFloat)_sumOfListIndents
{
CGFloat indent = 0;
DTHTMLElement *element = self.parentElement;
while (element)
{
if ([element.name isEqualToString:@"ul"] || [element.name isEqualToString:@"ol"])
{
indent += element->_listIndent;
}
else if (element.displayStyle == DTHTMLElementDisplayStyleListItem)
{
// we accept these
indent += element.padding.left;
}
else
{
break;
}
element = element.parentElement;
}
return indent;
}
- (void)applyStyleDictionary:(NSDictionary *)styles
{
[super applyStyleDictionary:styles];
CGFloat parentPadding = self.parentElement->_listIndent;
CGFloat listIndents = [self _sumOfListIndents];
self.paragraphStyle.headIndent = listIndents + _padding.left + _margins.left;
self.paragraphStyle.firstLineHeadIndent = self.paragraphStyle.headIndent;
_margins.left += parentPadding;
}
// creates an attributed list prefix
- (NSAttributedString *)_listPrefix
{
DTCoreTextParagraphStyle *paragraphStyle = [[self attributesForAttributedStringRepresentation] paragraphStyle];
NSParameterAssert(paragraphStyle);
DTCoreTextFontDescriptor *fontDescriptor = [[self attributesForAttributedStringRepresentation] fontDescriptor];
NSParameterAssert(fontDescriptor);
DTCSSListStyle *effectiveList = [self.paragraphStyle.textLists lastObject];
DTHTMLElement *listRoot = self.parentElement;
NSUInteger listCounter = [self _indexOfListItemInListRoot:listRoot]+effectiveList.startingItemNumber;
// make a temporary version of self that has same font attributes as list root
DTListItemHTMLElement *tmpCopy = [[DTListItemHTMLElement alloc] init];
[tmpCopy inheritAttributesFromElement:self];
// take the parents text color
tmpCopy.textColor = listRoot.textColor;
// check for list-style:none modifier
NSDictionary *styles = [[self attributeForKey:@"style"] dictionaryOfCSSStyles];
if (styles)
{
// make a temp copy
effectiveList = [effectiveList copy];
// update from styles
[effectiveList updateFromStyleDictionary:styles];
}
NSDictionary *attributes = [tmpCopy attributesForAttributedStringRepresentation];
// modify paragraph style
paragraphStyle.firstLineHeadIndent = self.paragraphStyle.headIndent - _margins.left - _padding.left;; // first line has prefix and starts at list indent;
paragraphStyle.defaultTabInterval = 100;
// resets tabs
paragraphStyle.tabStops = nil;
// set tab stops
if (effectiveList.type != DTCSSListStyleTypeNone)
{
if (_margins.left<=0)
{
return nil;
}
// first tab is to right-align bullet, numbering against
CGFloat tabOffset = paragraphStyle.headIndent - (CGFloat)5.0; // TODO: change with font size
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
[paragraphStyle addTabStopAtPosition:tabOffset alignment:kCTTextAlignmentRight];
#else
[paragraphStyle addTabStopAtPosition:tabOffset alignment:kCTRightTextAlignment];
#endif
}
// second tab is for the beginning of first line after bullet
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
[paragraphStyle addTabStopAtPosition:paragraphStyle.headIndent alignment:kCTTextAlignmentLeft];
#else
[paragraphStyle addTabStopAtPosition:paragraphStyle.headIndent alignment:kCTLeftTextAlignment];
#endif
NSMutableDictionary *newAttributes = [NSMutableDictionary dictionary];
// make a font without italic or bold
fontDescriptor.boldTrait = NO;
fontDescriptor.italicTrait = NO;
CTFontRef font = [fontDescriptor newMatchingFont];
if (font)
{
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_5_1
if (___useiOS6Attributes)
{
UIFont *uiFont = [UIFont fontWithCTFont:font];
[newAttributes setObject:uiFont forKey:NSFontAttributeName];
CFRelease(font);
}
else
#endif
{
[newAttributes setObject:CFBridgingRelease(font) forKey:(id)kCTFontAttributeName];
}
}
CGColorRef textColor = (__bridge CGColorRef)[attributes objectForKey:(id)kCTForegroundColorAttributeName];
if (textColor)
{
[newAttributes setObject:(__bridge id)textColor forKey:(id)kCTForegroundColorAttributeName];
}
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
else if (___useiOS6Attributes)
{
DTColor *uiColor = [attributes foregroundColor];
if (uiColor)
{
[newAttributes setObject:uiColor forKey:NSForegroundColorAttributeName];
}
}
#endif
// add paragraph style (this has the tabs)
if (paragraphStyle)
{
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
if (___useiOS6Attributes)
{
NSParagraphStyle *style = [paragraphStyle NSParagraphStyle];
[newAttributes setObject:style forKey:NSParagraphStyleAttributeName];
}
else
#endif
{
CTParagraphStyleRef newParagraphStyle = [paragraphStyle createCTParagraphStyle];
[newAttributes setObject:CFBridgingRelease(newParagraphStyle) forKey:(id)kCTParagraphStyleAttributeName];
}
}
// add textBlock if there's one (this has padding and background color)
NSArray *textBlocks = [attributes objectForKey:DTTextBlocksAttribute];
if (textBlocks)
{
[newAttributes setObject:textBlocks forKey:DTTextBlocksAttribute];
}
// transfer all lists so that
NSArray *lists = [attributes objectForKey:DTTextListsAttribute];
if (lists)
{
[newAttributes setObject:lists forKey:DTTextListsAttribute];
}
// add a marker so that we know that this is a field/prefix
[newAttributes setObject:DTListPrefixField forKey:DTFieldAttribute];
NSString *prefix = [effectiveList prefixWithCounter:listCounter];
if (!prefix)
{
return nil;
}
DTImage *image = nil;
if (effectiveList.imageName)
{
image = [DTImage imageNamed:effectiveList.imageName];
if (!image)
{
// image invalid
effectiveList.imageName = nil;
prefix = [effectiveList prefixWithCounter:listCounter];
}
}
if (!prefix)
{
return nil;
}
NSMutableAttributedString *tmpStr = [[NSMutableAttributedString alloc] initWithString:prefix attributes:newAttributes];
if (image)
{
// make an attachment for the image
DTImageTextAttachment *attachment = [[DTImageTextAttachment alloc] init];
attachment.image = image;
attachment.displaySize = image.size;
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && TARGET_OS_IPHONE
// need run delegate for sizing
CTRunDelegateRef embeddedObjectRunDelegate = createEmbeddedObjectRunDelegate(attachment);
[newAttributes setObject:CFBridgingRelease(embeddedObjectRunDelegate) forKey:(id)kCTRunDelegateAttributeName];
#endif
// add attachment
[newAttributes setObject:attachment forKey:NSAttachmentAttributeName];
if (effectiveList.position == DTCSSListStylePositionInside)
{
[tmpStr setAttributes:newAttributes range:NSMakeRange(2, 1)];
}
else
{
[tmpStr setAttributes:newAttributes range:NSMakeRange(1, 1)];
}
}
// estimate width of the prefix
NSString *trimmedPrefix = [prefix stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSAttributedString *tmpAttributedString = [[NSAttributedString alloc] initWithString:trimmedPrefix attributes:newAttributes];
CTLineRef tmpLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)(tmpAttributedString));
double width = CTLineGetTypographicBounds(tmpLine, NULL, NULL, NULL);
CFRelease(tmpLine);
// if the non-whitespace characters are too wide then we omit the prefix
if ((width+5.0)>_margins.left)
{
return nil;
}
return tmpStr;
}
- (NSAttributedString *)attributedString
{
NSMutableAttributedString *tmpString = [[NSMutableAttributedString alloc] init];
// append child elements
NSAttributedString *childrenString = [super attributedString];
// append list prefix
NSAttributedString *listPrefix = [self _listPrefix];
if (listPrefix)
{
[tmpString appendAttributedString:listPrefix];
// add NL if there is immediately another list prefix following
NSString *field = [childrenString attribute:DTFieldAttribute atIndex:0 effectiveRange:NULL];
if ([field isEqualToString:DTListPrefixField])
{
[tmpString appendEndOfParagraph];
}
}
if (childrenString)
{
[tmpString appendAttributedString:childrenString];
}
return tmpString;
}
@end
@@ -1,22 +0,0 @@
//
// DTObjectTextAttachment.h
// DTCoreText
//
// Created by Oliver Drobnik on 22.04.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTTextAttachment.h"
/**
A specialized subclass in the DTTextAttachment class cluster to represent an generic object
*/
@interface DTObjectTextAttachment : DTTextAttachment <DTTextAttachmentHTMLPersistence>
/**
The DTHTMLElement child nodes of the receiver. This array is only used for object tags at the moment.
*/
@property (nonatomic, strong) NSArray *childNodes;
@end
@@ -1,142 +0,0 @@
//
// DTObjectTextAttachment.m
// DTCoreText
//
// Created by Oliver Drobnik on 22.04.13.
// Copyright (c) 2013 Drobnik.com. All rights reserved.
//
#import "DTObjectTextAttachment.h"
#import "DTCoreTextConstants.h"
#import "DTHTMLElement.h"
#import "NSString+HTML.h"
@implementation DTObjectTextAttachment
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_childNodes = [aDecoder decodeObjectForKey:@"childNodes"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeObject:_childNodes forKey:@"childNodes"];
}
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
{
self = [super initWithElement:element options:options];
if (self)
{
// get base URL
NSURL *baseURL = [options objectForKey:NSBaseURLDocumentOption];
NSString *src = [element.attributes objectForKey:@"src"];
// content URL
_contentURL = [NSURL URLWithString:src relativeToURL:baseURL];
}
return self;
}
#pragma mark - DTTextAttachmentHTMLEncoding
- (NSString *)stringByEncodingAsHTML
{
NSMutableString *retString = [NSMutableString string];
[retString appendString:@"<object"];
if (_contentURL)
{
[retString appendFormat:@" src=\"%@\"", [_contentURL absoluteString]];
}
// build style for img/video
NSMutableString *styleString = [NSMutableString string];
switch (_verticalAlignment)
{
case DTTextAttachmentVerticalAlignmentBaseline:
{
// [classStyleString appendString:@"vertical-align:baseline;"];
break;
}
case DTTextAttachmentVerticalAlignmentTop:
{
[styleString appendString:@"vertical-align:text-top;"];
break;
}
case DTTextAttachmentVerticalAlignmentCenter:
{
[styleString appendString:@"vertical-align:middle;"];
break;
}
case DTTextAttachmentVerticalAlignmentBottom:
{
[styleString appendString:@"vertical-align:text-bottom;"];
break;
}
}
if (_originalSize.width>0)
{
[styleString appendFormat:@"width:%.0fpx;", _originalSize.width];
}
if (_originalSize.height>0)
{
[styleString appendFormat:@"height:%.0fpx;", _originalSize.height];
}
// add local style for size, since sizes might vary quite a bit
if ([styleString length])
{
[retString appendFormat:@" style=\"%@\"", styleString];
}
// attach the attributes dictionary
NSMutableDictionary *tmpAttributes = [_attributes mutableCopy];
// remove src,style, width and height we already have these
[tmpAttributes removeObjectForKey:@"src"];
[tmpAttributes removeObjectForKey:@"style"];
[tmpAttributes removeObjectForKey:@"width"];
[tmpAttributes removeObjectForKey:@"height"];
for (__strong NSString *oneKey in [tmpAttributes allKeys])
{
oneKey = [oneKey stringByAddingHTMLEntities];
NSString *value = [[tmpAttributes objectForKey:oneKey] stringByAddingHTMLEntities];
[retString appendFormat:@" %@=\"%@\"", oneKey, value];
}
if (_childNodes)
{
[retString appendString:@">"];
for (DTHTMLElement *oneChild in _childNodes)
{
[retString appendString:[oneChild debugDescription]];
}
[retString appendString:@"</object>"];
}
else
{
[retString appendString:@" />"];
}
return retString;
}
#pragma mark - Properties
@synthesize childNodes = _childNodes;
@end
@@ -1,23 +0,0 @@
//
// DTHTMLElementStylesheet.h
// DTCoreText
//
// Created by Oliver Drobnik on 29.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHTMLElement.h"
@class DTCSSStylesheet;
/**
This is a specialized subclass of <DTHTMLElement> representing a style block.
*/
@interface DTStylesheetHTMLElement : DTHTMLElement
/**
Parses the text children and assembles the resulting stylesheet.
*/
- (DTCSSStylesheet *)stylesheet;
@end
@@ -1,26 +0,0 @@
//
// DTHTMLElementStylesheet.m
// DTCoreText
//
// Created by Oliver Drobnik on 29.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTStylesheetHTMLElement.h"
#import "DTCSSStylesheet.h"
@implementation DTStylesheetHTMLElement
- (NSAttributedString *)attributedString
{
return nil;
}
- (DTCSSStylesheet *)stylesheet
{
NSString *text = [self text];
return [[DTCSSStylesheet alloc] initWithStyleBlock:text];
}
@end
@@ -1,199 +0,0 @@
//
// DTTextAttachment.h
// DTCoreText
//
// Created by Oliver on 14.01.11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTCompatibility.h"
#if TARGET_OS_IPHONE
#elif TARGET_OS_MAC
#import <ApplicationServices/ApplicationServices.h>
#endif
#import <CoreText/CoreText.h>
@class DTHTMLElement;
/**
Text Attachment vertical alignment
*/
typedef NS_ENUM(NSUInteger, DTTextAttachmentVerticalAlignment)
{
/**
Baseline alignment (default)
*/
DTTextAttachmentVerticalAlignmentBaseline = 0,
/**
Align with top edge
*/
DTTextAttachmentVerticalAlignmentTop,
/**
Align with center
*/
DTTextAttachmentVerticalAlignmentCenter,
/**
Align with bottom edge
*/
DTTextAttachmentVerticalAlignmentBottom
};
/**
Methods to implement for attachments to support inline drawing.
*/
@protocol DTTextAttachmentDrawing <NSObject>
/**
Draws the contents of the receiver into a graphics context
@param rect The rectangle to draw the receiver into
@param context The graphics context
*/
- (void)drawInRect:(CGRect)rect context:(CGContextRef)context;
@end
/**
Methods to implement for attachments to support output to HTML.
*/
@protocol DTTextAttachmentHTMLPersistence <NSObject>
/**
Creates a HTML representation of the receiver
@returns A HTML string with the receiver encoded as HTML
*/
- (NSString *)stringByEncodingAsHTML;
@end
/**
An object to represent an attachment in an HTML/rich text view.
*/
@interface DTTextAttachment : NSTextAttachment
{
CGSize _displaySize; // the display dimensions of the attachment
CGSize _originalSize; // the original dimensions of the attachment
CGSize _maxImageSize; // the maximum dimensions to size to
NSURL *_contentURL;
NSDictionary *_attributes; // attributes transferred from HTML element
DTTextAttachmentVerticalAlignment _verticalAlignment; // alignment in relation to the baseline
}
/**
@name Creating Text Attachments
*/
/**
Initialize and return a DTTextAttachment with the specified DTHTMLElement and options. Convenience initializer.
The element must have a valid tagName. The size of the returned text attachment is determined by the element, constrained by the option's key for DTMaxImageSize. Any valid image resource included in the element (denoted by the method attributeForKey: "src") is loaded and determines the text attachment size if it was not known before. If a size is too large the image is downsampled with sizeThatFitsKeepingAspectRatio() which preserves the aspect ratio.
@param element A DTHTMLElement that must have a valid tag name and should have a size. Any element attributes are copied to the text attachment's elements.
@param options An NSDictionary of options. Used to specify the max image size with the key DTMaxImageSize.
@returns Returns the appropriate subclass of the class cluster
*/
+ (DTTextAttachment *)textAttachmentWithElement:(DTHTMLElement *)element options:(NSDictionary *)options;
/**
The designated initializer for members of the DTTextAttachment class cluster. If you need additional setup for custom subclasses then you should override this initializer.
@param element A DTHTMLElement that must have a valid tag name and should have a size. Any element attributes are copied to the text attachment's elements.
@param options An NSDictionary of options. Used to specify the max image size with the key DTMaxImageSize.
@returns Returns an initialized DTTextAttachment built using the element and options parameters. */
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options;
/**
@name Vertical Alignment
*/
/**
Inspects the given font and records the font's ascent, descent and leading. These values are then used during layout to properly respond with ascentForLayout, descentForLayout for the receiver's verticalAlignment.
@param font The font to inspect
*/
- (void)adjustVerticalAlignmentForFont:(CTFontRef)font;
/**
The ascent to use during layout so that the receiver can be display at its verticalAlignment.
*/
- (CGFloat)ascentForLayout;
/**
The descent to use during layout so that the receiver can be display at its verticalAlignment.
*/
- (CGFloat)descentForLayout;
/**
The vertical alignment of the receiver
*/
@property (nonatomic, assign) DTTextAttachmentVerticalAlignment verticalAlignment;
/**
@name Retrieving Information about Attachments
*/
/**
The size of the receiver according to width/height HTML attribute or CSS style attribute
*/
@property (nonatomic, assign) CGSize originalSize;
/**
The size to use for displaying/laying out the receiver
*/
@property (nonatomic, assign) CGSize displaySize;
/**
Updates the display size optionally passing a maximum size that it should not exceed.
This method in contrast to using the displaySize property will use the originalSize and max display size to calculate missing dimensions.
@param displaySize The new size to display the content with
@param maxDisplaySize the maximum size that the content should be scaled to fit
*/
- (void)setDisplaySize:(CGSize)displaySize withMaxDisplaySize:(CGSize)maxDisplaySize;
/**
The URL representing the content
*/
@property (nonatomic, strong) NSURL *contentURL;
/**
The hyperlink URL of the receiver.
*/
@property (nonatomic, strong) NSURL *hyperLinkURL;
/**
The GUID of the hyperlink that this is a part of. All parts of a hyperlink have the same GUID so that they can highlight together.
*/
@property (nonatomic, strong) NSString *hyperLinkGUID;
/**
The attributes dictionary of the attachment.
If initialized from HTML, the values of this dictionary are transferred from the give HTML element. If you wish to add custom attribute values to be written to and read from HTML, be aware that the attribute name will be lowercased in compliance with W3C recommendations. Therefore, you may set a camel-case name and persist to HTML, but you will receive a lowercase name when the HTML is transformed into an attributed string.
*/
@property (nonatomic, strong) NSDictionary *attributes;
/**
@name Customizing Attachments
*/
/**
Registers your own class for use when encountering a specific tag Name. If you register a class for a previously registered class (or one of the predefined ones (img, iframe, object, video) then this replaces this with the newer registration.
These registrations are permanent during the run time of your app. Custom attachment classes must implement the initWithElement:options: initializer and can implement the DTTextAttachmentDrawing and/or DTTextAttachmentHTMLPersistence protocols.
@param theClass The class to instantiate in textAttachmentWithElement:options: when encountering a tag with this name
@param tagName The tag name to use this class for
*/
+ (void)registerClass:(Class)theClass forTagName:(NSString *)tagName;
/**
The class to use for a tag name
@param tagName The tag name
@returns The class to use for attachments with with tag name, or `nil` if this should not be an attachment
*/
+ (Class)registeredClassForTagName:(NSString *)tagName;
@end
@@ -1,263 +0,0 @@
//
// DTTextAttachment.m
// DTCoreText
//
// Created by Oliver on 14.01.11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DTTextAttachment.h"
#import "DTHTMLElement.h"
#import "DTDictationPlaceholderTextAttachment.h"
#import "DTIframeTextAttachment.h"
#import "DTImageTextAttachment.h"
#import "DTObjectTextAttachment.h"
#import "DTVideoTextAttachment.h"
#import "NSCoder+DTCompatibility.h"
#import <DTFoundation/DTLog.h>
#import <DTFoundation/DTCoreGraphicsUtils.h>
static NSMutableDictionary *_classForTagNameLookup = nil;
@interface DTTextAttachment ()
@end
@implementation DTTextAttachment
{
NSURL *_hyperLinkURL;
NSString *_hyperLinkGUID;
CGFloat _fontLeading;
CGFloat _fontAscent;
CGFloat _fontDescent;
}
+ (void)initialize
{
// this gets called from each subclass
// prevent calling from children
if (self != [DTTextAttachment class])
{
return;
}
_classForTagNameLookup = [[NSMutableDictionary alloc] init];
// register standard tags
[DTTextAttachment registerClass:[DTImageTextAttachment class] forTagName:@"img"];
[DTTextAttachment registerClass:[DTVideoTextAttachment class] forTagName:@"video"];
[DTTextAttachment registerClass:[DTIframeTextAttachment class] forTagName:@"iframe"];
[DTTextAttachment registerClass:[DTObjectTextAttachment class] forTagName:@"object"];
}
+ (DTTextAttachment *)textAttachmentWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
{
Class class = [DTTextAttachment registeredClassForTagName:element.name];
if (!class)
{
return nil;
}
DTTextAttachment *attachment = [class alloc];
return [attachment initWithElement:element options:options];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
_displaySize = [aDecoder decodeCGSizeForKey:@"displaySize"];
_originalSize = [aDecoder decodeCGSizeForKey:@"originalSize"];
_maxImageSize = [aDecoder decodeCGSizeForKey:@"maxImageSize"];
_contentURL = [aDecoder decodeObjectForKey:@"contentURL"];
_attributes = [aDecoder decodeObjectForKey:@"attributes"];
_verticalAlignment = [aDecoder decodeIntegerForKey:@"verticalAlignment"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeCGSize:_displaySize forKey:@"displaySize"];
[aCoder encodeCGSize:_originalSize forKey:@"originalSize"];
[aCoder encodeCGSize:_maxImageSize forKey:@"maxImageSize"];
[aCoder encodeObject:_contentURL forKey:@"contentURL"];
[aCoder encodeObject:_attributes forKey:@"attributes"];
[aCoder encodeInteger:_verticalAlignment forKey:@"verticalAlignment"];
}
- (id)initWithElement:(DTHTMLElement *)element options:(NSDictionary *)options
{
self = [super init];
if (self)
{
// width, height from tag
_originalSize = element.size; // initially not known
// determine if there is a display size restriction
_maxImageSize = CGSizeZero;
NSValue *maxImageSizeValue =[options objectForKey:DTMaxImageSize];
if (maxImageSizeValue)
{
#if TARGET_OS_IPHONE
_maxImageSize = [maxImageSizeValue CGSizeValue];
#else
_maxImageSize = [maxImageSizeValue sizeValue];
#endif
}
// set the display size from the original size, restricted to the max size
[self setDisplaySize:_originalSize withMaxDisplaySize:_maxImageSize];
_attributes = element.attributes;
}
return self;
}
- (void)adjustVerticalAlignmentForFont:(CTFontRef)font
{
_fontLeading = CTFontGetLeading(font);
_fontAscent = CTFontGetAscent(font);
_fontDescent = CTFontGetDescent(font);
}
- (CGFloat)ascentForLayout
{
switch (_verticalAlignment)
{
case DTTextAttachmentVerticalAlignmentBaseline:
{
return _displaySize.height;
}
case DTTextAttachmentVerticalAlignmentTop:
{
return _fontAscent;
}
case DTTextAttachmentVerticalAlignmentCenter:
{
CGFloat halfHeight = (_fontAscent + _fontDescent) / 2.0f;
return halfHeight - _fontDescent + _displaySize.height/2.0f;
}
case DTTextAttachmentVerticalAlignmentBottom:
{
return _displaySize.height - _fontDescent;
}
}
}
- (CGFloat)descentForLayout
{
switch (_verticalAlignment)
{
case DTTextAttachmentVerticalAlignmentBaseline:
{
return 0;
}
case DTTextAttachmentVerticalAlignmentTop:
{
return _displaySize.height - _fontAscent;
}
case DTTextAttachmentVerticalAlignmentCenter:
{
CGFloat halfHeight = (_fontAscent + _fontDescent) / 2.0f;
return halfHeight - _fontAscent + _displaySize.height/2.0f;
}
case DTTextAttachmentVerticalAlignmentBottom:
{
return _fontDescent;
}
}
}
#pragma mark - Subclass Customization
+ (void)registerClass:(Class)class forTagName:(NSString *)tagName
{
Class previousClass = [DTTextAttachment registeredClassForTagName:tagName];
if (previousClass)
{
DTLogDebug(@"Replacing previously registered class '%@' for tag name '%@' with '%@'", NSStringFromClass(previousClass), tagName, NSStringFromClass(class));
}
[_classForTagNameLookup setObject:class forKey:tagName];
}
+ (Class)registeredClassForTagName:(NSString *)tagName
{
return [_classForTagNameLookup objectForKey:tagName];
}
#pragma mark Properties
/** Mutator for originalSize. Sets displaySize to the same value as originalSize.
@param originalSize The CGSize to store in originalSize. */
- (void)setOriginalSize:(CGSize)originalSize
{
if (!CGSizeEqualToSize(originalSize, _originalSize))
{
_originalSize = originalSize;
if (_displaySize.width==0 || _displaySize.height==0)
{
[self setDisplaySize:_originalSize withMaxDisplaySize:_maxImageSize];
}
}
}
- (void)setDisplaySize:(CGSize)displaySize withMaxDisplaySize:(CGSize)maxDisplaySize
{
if (_originalSize.width!=0 && _originalSize.height!=0)
{
// width and/or height missing
if (displaySize.width==0 && displaySize.height==0)
{
displaySize = _originalSize;
}
else if (displaySize.width==0 && displaySize.height!=0)
{
// width missing, calculate it
CGFloat factor = _originalSize.height / displaySize.height;
displaySize.width = round(_originalSize.width / factor);
}
else if (displaySize.width!=0 && displaySize.height==0)
{
// height missing, calculate it
CGFloat factor = _originalSize.width / displaySize.width;
displaySize.height = round(_originalSize.height / factor);
}
}
if (maxDisplaySize.width>0 && maxDisplaySize.height>0)
{
if (maxDisplaySize.width < displaySize.width || maxDisplaySize.height < displaySize.height)
{
displaySize = DTCGSizeThatFitsKeepingAspectRatio(displaySize, maxDisplaySize);
}
}
_displaySize = displaySize;
}
- (void)setDisplaySize:(CGSize)displaySize
{
_displaySize = displaySize;
}
@synthesize originalSize = _originalSize;
@synthesize displaySize = _displaySize;
@synthesize contentURL = _contentURL;
@synthesize hyperLinkURL = _hyperLinkURL;
@synthesize attributes = _attributes;
@synthesize verticalAlignment = _verticalAlignment;
@synthesize hyperLinkGUID = hyperLinkGUID;
@end
@@ -1,17 +0,0 @@
//
// DTHTMLElementAttachment.h
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTHTMLElement.h"
/**
Specialized subclass of <DTHTMLElement> for dealing with <DTTextAttachment> instances, e.g. images.
*/
@interface DTTextAttachmentHTMLElement : DTHTMLElement
@end
@@ -1,123 +0,0 @@
//
// DTHTMLElementAttachment.m
// DTCoreText
//
// Created by Oliver Drobnik on 26.12.12.
// Copyright (c) 2012 Drobnik.com. All rights reserved.
//
#import "DTTextAttachmentHTMLElement.h"
#import "DTHTMLElement.h"
#import "DTTextAttachment.h"
#import "DTCoreTextParagraphStyle.h"
#import "NSMutableAttributedString+HTML.h"
@implementation DTTextAttachmentHTMLElement
{
CGSize _maxDisplaySize;
}
- (id)initWithName:(NSString *)name attributes:(NSDictionary *)attributes options:(NSDictionary *)options
{
self = [super initWithName:name attributes:attributes];
if (self)
{
// make appropriate attachment
DTTextAttachment *attachment = [DTTextAttachment textAttachmentWithElement:self options:options];
// add it to tag
_textAttachment = attachment;
// to avoid much too much space before the image
if (nil == _paragraphStyle)
_paragraphStyle = [[DTCoreTextParagraphStyle alloc] init];
_paragraphStyle.lineHeightMultiple = 1;
// specifying line height interferes with correct positioning
_paragraphStyle.minimumLineHeight = 0;
_paragraphStyle.maximumLineHeight = 0;
// remember the maximum display size
_maxDisplaySize = CGSizeZero;
NSValue *maxImageSizeValue =[options objectForKey:DTMaxImageSize];
if (maxImageSizeValue)
{
#if TARGET_OS_IPHONE
_maxDisplaySize = [maxImageSizeValue CGSizeValue];
#else
_maxDisplaySize = [maxImageSizeValue sizeValue];
#endif
}
}
return self;
}
- (NSAttributedString *)attributedString
{
@synchronized(self)
{
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
// ignore text, use unicode object placeholder
NSMutableAttributedString *tmpString = [[NSMutableAttributedString alloc] initWithString:UNICODE_OBJECT_PLACEHOLDER attributes:attributes];
// block-level elements get space trimmed and a newline
if (self.displayStyle != DTHTMLElementDisplayStyleInline)
{
[tmpString appendString:@"\n"];
}
return tmpString;
}
}
// workaround, because we don't support float yet. float causes the image to be its own block
- (DTHTMLElementDisplayStyle)displayStyle
{
if ([super floatStyle]==DTHTMLElementFloatStyleNone)
{
return [super displayStyle];
}
return DTHTMLElementDisplayStyleBlock;
}
- (void)applyStyleDictionary:(NSDictionary *)styles
{
// element size is determined in super (tag attribute and style)
[super applyStyleDictionary:styles];
// at this point we have the size from width/height attribute or style in _size
// set original size if it was previously unknown
if (CGSizeEqualToSize(CGSizeZero, _textAttachment.originalSize))
{
_textAttachment.originalSize = _size;
}
NSString *widthString = [styles objectForKey:@"width"];
NSString *heightString = [styles objectForKey:@"height"];
if (widthString.length > 1 && [widthString hasSuffix:@"%"])
{
CGFloat scale = (CGFloat)([[widthString substringToIndex:widthString.length - 1] floatValue] / 100.0);
_size.width = _maxDisplaySize.width * scale;
}
if (heightString.length > 1 && [heightString hasSuffix:@"%"])
{
CGFloat scale = (CGFloat)([[heightString substringToIndex:heightString.length - 1] floatValue] / 100.0);
_size.height = _maxDisplaySize.height * scale;
}
// update the display size
[_textAttachment setDisplaySize:_size withMaxDisplaySize:_maxDisplaySize];
}
@end

Some files were not shown because too many files have changed in this diff Show More