oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import AppKit
3
4extension NSAttributedString.Key {
5 static let docsTopBlock = NSAttributedString.Key("OficinaDocsTopBlock")
6 static let docsMarginBefore = NSAttributedString.Key("OficinaDocsMarginBefore")
7 static let docsMarginAfter = NSAttributedString.Key("OficinaDocsMarginAfter")
8}
9
10/// Mirrors pages.mjs: keep top-level blocks intact and put a visual separator
11/// before the next block exceeding the 648pt text budget. All offsets are layout
12/// only; an oversized paragraph/list is never split or rewritten to make a page.
13@MainActor enum DocumentPageLayout {
14 struct Result {
15 var starts: [Int: CGFloat] = [:]
16 var separators: [CGFloat] = []
17 }
18 /// Mapping IDs, formatting marks and colors do not affect glyph positions.
19 /// Retain only layout inputs so metadata acknowledgements reuse pagination.
20 static func measurementContent(_ storage: NSAttributedString) -> NSAttributedString {
21 let keys: Set<NSAttributedString.Key> = [.font, .paragraphStyle, .kern, .ligature, .baselineOffset,
22 .attachment, .expansion, .obliqueness, .writingDirection, .verticalGlyphForm,
23 .superscript, .docsBaseFont, .docsLeading, .docsCodeStart, .docsFirstMargin,
24 .docsTopBlock, .docsMarginBefore, .docsMarginAfter]
25 let result = NSMutableAttributedString(string: storage.string)
26 storage.enumerateAttributes(in: NSRange(location: 0, length: storage.length)) { attributes, range, _ in
27 result.setAttributes(attributes.filter { keys.contains($0.key) }, range: range)
28 }
29 return result
30 }
31 static func measure(storage: NSTextStorage, manager: NSLayoutManager, container: NSTextContainer) -> Result {
32 var result = Result()
33 var pageTop: CGFloat?
34 var added: CGFloat = 0
35 var previousMargin: CGFloat = 0
36 storage.enumerateAttribute(.docsTopBlock, in: NSRange(location: 0, length: storage.length)) { value, range, _ in
37 guard value != nil else { return }
38 let glyphs = manager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
39 var top = CGFloat.greatestFiniteMagnitude
40 var bottom: CGFloat = 0
41 manager.enumerateLineFragments(forGlyphRange: glyphs) { rect, used, _, _, _ in
42 top = min(top, rect.minY); bottom = max(bottom, used.maxY)
43 }
44 guard bottom > top else { return }
45 let before = storage.attribute(.docsMarginBefore, at: range.location, effectiveRange: nil) as? CGFloat ?? 0
46 let after = storage.attribute(.docsMarginAfter, at: range.location, effectiveRange: nil) as? CGFloat ?? 0
47 defer { previousMargin = after }
48 if let currentTop = pageTop {
49 if bottom - currentTop > 648 {
50 // Collapse margins independently on either side of the gap.
51 let collapsed = max(previousMargin, before)
52 let separator = top + added - collapsed + max(previousMargin, 12)
53 let extra = max(previousMargin, 12) + 18 + max(before, 12) - collapsed
54 added += extra
55 result.starts[range.location] = top + added
56 result.separators.append(separator)
57 pageTop = top
58 }
59 } else { pageTop = top }
60 }
61 return result
62 }
63}
64#endif
65