react-native-marcus 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -63
- package/RNMarcus.podspec +6 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +2 -1
- package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorView.kt +168 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManager.kt +38 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManagerSpec.kt +19 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +23 -1
- package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPainter.kt +147 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPass.kt +19 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownCopy.kt +87 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +652 -113
- package/android/src/main/java/app/getbullet/marcus/MarkdownRanges.kt +47 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +140 -61
- package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +37 -7
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownAbsoluteLineHeightSpan.kt +48 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +102 -48
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +22 -4
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBulletSpan.kt +78 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpan.kt +177 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontStyleSpan.kt +32 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontWeightSpan.kt +34 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +24 -11
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLetterSpacingSpan.kt +32 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownTextShadowSpan.kt +32 -0
- package/android/src/main/new_arch/CMakeLists.txt +17 -0
- package/android/src/main/new_arch/RNMarcusSpec.h +1 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.cpp +235 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.h +62 -0
- package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
- package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
- package/android/src/test/java/app/getbullet/marcus/MarkdownCopyTest.kt +69 -0
- package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +170 -25
- package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +22 -5
- package/android/src/test/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpanTest.kt +91 -0
- package/android/src/test/resources/render-model.txt +221 -16
- package/apple/MarcusSwizzle.h +25 -0
- package/apple/MarcusTextDecoratorComponentView.h +22 -0
- package/apple/MarcusTextDecoratorComponentView.mm +72 -0
- package/apple/MarcusTextDecoratorShadowNode.h +62 -0
- package/apple/MarcusTextDecoratorShadowNode.mm +192 -0
- package/apple/MarcusTextDecoratorViewManager.h +4 -0
- package/apple/MarcusTextDecoratorViewManager.mm +11 -0
- package/apple/MarcusTextInputDecoratorComponentView.mm +1 -1
- package/apple/MarcusTextInputDecoratorShadowNode.mm +1 -1
- package/apple/MarcusTextLayoutManager.h +40 -0
- package/apple/MarcusTextLayoutManager.mm +173 -0
- package/apple/MarkdownAttributes.h +23 -2
- package/apple/MarkdownAttributes.m +6 -0
- package/apple/MarkdownCopy.swift +63 -0
- package/apple/MarkdownDecorator.swift +2 -2
- package/apple/MarkdownFormatter+React.swift +23 -0
- package/apple/MarkdownFormatter.swift +869 -148
- package/apple/MarkdownParagraphLayoutManager.swift +455 -0
- package/apple/MarkdownSwiftInterop.h +0 -1
- package/apple/MarkdownTextBackground.swift +59 -1
- package/apple/MarkdownTextFieldObserver.swift +2 -2
- package/apple/MarkdownTextLayoutFragment.swift +159 -33
- package/apple/MarkdownTextLayoutManagerDelegate.swift +67 -3
- package/apple/MarkdownTextStorageDelegate.swift +2 -2
- package/apple/MarkdownTextViewObserver.swift +2 -2
- package/apple/MarkdownUtils.swift +38 -0
- package/apple/MarkdownWritingDirection.swift +132 -0
- package/apple/RCTMarcusStyle+Codegen.h +115 -9
- package/apple/RCTMarcusStyle.h +128 -31
- package/apple/RCTMarcusStyle.m +48 -0
- package/apple/RCTParagraphComponentView+Marcus.h +17 -0
- package/apple/RCTParagraphComponentView+Marcus.mm +38 -0
- package/apple/RCTTextInputComponentView+Marcus.mm +1 -25
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorState.h +37 -0
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorViewComponentDescriptor.h +17 -0
- package/lib/commonjs/MarcusTextDecoratorViewNativeComponent.ts +199 -0
- package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
- package/lib/commonjs/MarkdownText.js +330 -0
- package/lib/commonjs/MarkdownText.js.map +1 -0
- package/lib/commonjs/MarkdownText.web.js +529 -0
- package/lib/commonjs/MarkdownText.web.js.map +1 -0
- package/lib/commonjs/MarkdownTextInput.js +22 -27
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/MarkdownTextInput.web.js +12 -7
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/atomUtils.js +112 -0
- package/lib/commonjs/atomUtils.js.map +1 -0
- package/lib/commonjs/childrenUtils.js +36 -0
- package/lib/commonjs/childrenUtils.js.map +1 -0
- package/lib/commonjs/index.js +8 -7
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/math/environment.js +69 -0
- package/lib/commonjs/math/environment.js.map +1 -0
- package/lib/commonjs/math/index.js +61 -0
- package/lib/commonjs/math/index.js.map +1 -0
- package/lib/commonjs/math/mathjax.js +548 -0
- package/lib/commonjs/math/mathjax.js.map +1 -0
- package/lib/commonjs/metro.js +172 -0
- package/lib/commonjs/metro.js.map +1 -0
- package/lib/commonjs/parser/index.d.ts +3 -0
- package/lib/commonjs/parser/index.js +450 -42
- package/lib/commonjs/parser/index.js.map +4 -4
- package/lib/commonjs/stripUtils.js +743 -0
- package/lib/commonjs/stripUtils.js.map +1 -0
- package/lib/commonjs/styleUtils.js +194 -23
- package/lib/commonjs/styleUtils.js.map +1 -1
- package/lib/commonjs/web/MarkdownElements.css +67 -0
- package/lib/commonjs/web/MarkdownTextInput.css +9 -7
- package/lib/commonjs/web/utils/blockLayout.js +95 -7
- package/lib/commonjs/web/utils/blockLayout.js.map +1 -1
- package/lib/commonjs/web/utils/blockUtils.js +327 -54
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
- package/lib/commonjs/web/utils/elementUtils.js +80 -0
- package/lib/commonjs/web/utils/elementUtils.js.map +1 -0
- package/lib/commonjs/web/utils/parserUtils.js +84 -28
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
- package/lib/commonjs/web/utils/treeUtils.js +6 -0
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
- package/lib/commonjs/web/utils/webStyleUtils.js +25 -5
- package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
- package/lib/module/MarcusTextDecoratorViewNativeComponent.ts +199 -0
- package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
- package/lib/module/MarkdownText.js +325 -0
- package/lib/module/MarkdownText.js.map +1 -0
- package/lib/module/MarkdownText.web.js +524 -0
- package/lib/module/MarkdownText.web.js.map +1 -0
- package/lib/module/MarkdownTextInput.js +24 -29
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +12 -7
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/atomUtils.js +106 -0
- package/lib/module/atomUtils.js.map +1 -0
- package/lib/module/childrenUtils.js +33 -0
- package/lib/module/childrenUtils.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/math/environment.js +67 -0
- package/lib/module/math/environment.js.map +1 -0
- package/lib/module/math/index.js +56 -0
- package/lib/module/math/index.js.map +1 -0
- package/lib/module/math/mathjax.js +541 -0
- package/lib/module/math/mathjax.js.map +1 -0
- package/lib/module/metro.js +169 -0
- package/lib/module/metro.js.map +1 -0
- package/lib/module/parser/index.d.ts +3 -0
- package/lib/module/parser/index.js +450 -42
- package/lib/module/parser/index.js.map +4 -4
- package/lib/module/stripUtils.js +739 -0
- package/lib/module/stripUtils.js.map +1 -0
- package/lib/module/styleUtils.js +196 -25
- package/lib/module/styleUtils.js.map +1 -1
- package/lib/module/web/MarkdownElements.css +67 -0
- package/lib/module/web/MarkdownTextInput.css +9 -7
- package/lib/module/web/utils/blockLayout.js +94 -8
- package/lib/module/web/utils/blockLayout.js.map +1 -1
- package/lib/module/web/utils/blockUtils.js +328 -56
- package/lib/module/web/utils/blockUtils.js.map +1 -1
- package/lib/module/web/utils/elementUtils.js +75 -0
- package/lib/module/web/utils/elementUtils.js.map +1 -0
- package/lib/module/web/utils/parserUtils.js +86 -30
- package/lib/module/web/utils/parserUtils.js.map +1 -1
- package/lib/module/web/utils/treeUtils.js +6 -0
- package/lib/module/web/utils/treeUtils.js.map +1 -1
- package/lib/module/web/utils/webStyleUtils.js +25 -5
- package/lib/module/web/utils/webStyleUtils.js.map +1 -1
- package/lib/typescript/commonjs/e2e/links.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/links.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
- package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/MarkdownText.d.ts +127 -0
- package/lib/typescript/commonjs/src/MarkdownText.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownText.web.d.ts +126 -0
- package/lib/typescript/commonjs/src/MarkdownText.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +23 -1
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +26 -4
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/atomUtils.d.ts +58 -0
- package/lib/typescript/commonjs/src/atomUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/childrenUtils.d.ts +12 -0
- package/lib/typescript/commonjs/src/childrenUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/commonTypes.d.ts +39 -3
- package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +2 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/math/environment.d.ts +7 -0
- package/lib/typescript/commonjs/src/math/environment.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/math/index.d.ts +64 -0
- package/lib/typescript/commonjs/src/math/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/math/mathjax.d.ts +63 -0
- package/lib/typescript/commonjs/src/math/mathjax.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/metro.d.ts +59 -0
- package/lib/typescript/commonjs/src/metro.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/parser/index.d.ts +3 -3
- package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/rangeUtils.d.ts +1 -1
- package/lib/typescript/commonjs/src/stripUtils.d.ts +108 -0
- package/lib/typescript/commonjs/src/stripUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/styleUtils.d.ts +62 -4
- package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +32 -4
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +20 -8
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts +37 -0
- package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +5 -5
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +6 -1
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +2 -2
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -1
- package/lib/typescript/module/e2e/links.test.d.ts +2 -0
- package/lib/typescript/module/e2e/links.test.d.ts.map +1 -0
- package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
- package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/MarkdownText.d.ts +127 -0
- package/lib/typescript/module/src/MarkdownText.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownText.web.d.ts +126 -0
- package/lib/typescript/module/src/MarkdownText.web.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownTextInput.d.ts +23 -1
- package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +26 -4
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/module/src/atomUtils.d.ts +58 -0
- package/lib/typescript/module/src/atomUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/childrenUtils.d.ts +12 -0
- package/lib/typescript/module/src/childrenUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/commonTypes.d.ts +39 -3
- package/lib/typescript/module/src/commonTypes.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +2 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/math/environment.d.ts +7 -0
- package/lib/typescript/module/src/math/environment.d.ts.map +1 -0
- package/lib/typescript/module/src/math/index.d.ts +64 -0
- package/lib/typescript/module/src/math/index.d.ts.map +1 -0
- package/lib/typescript/module/src/math/mathjax.d.ts +63 -0
- package/lib/typescript/module/src/math/mathjax.d.ts.map +1 -0
- package/lib/typescript/module/src/metro.d.ts +59 -0
- package/lib/typescript/module/src/metro.d.ts.map +1 -0
- package/lib/typescript/module/src/parser/index.d.ts +3 -3
- package/lib/typescript/module/src/parser/index.d.ts.map +1 -1
- package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -1
- package/lib/typescript/module/src/rangeUtils.d.ts +1 -1
- package/lib/typescript/module/src/stripUtils.d.ts +108 -0
- package/lib/typescript/module/src/stripUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/styleUtils.d.ts +62 -4
- package/lib/typescript/module/src/styleUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts +32 -4
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts +20 -8
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/elementUtils.d.ts +37 -0
- package/lib/typescript/module/src/web/utils/elementUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts +5 -5
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts +6 -1
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +2 -2
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -1
- package/package.json +29 -2
- package/react-native.config.js +8 -1
- package/src/MarcusTextDecoratorViewNativeComponent.ts +199 -0
- package/src/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
- package/src/MarkdownText.tsx +472 -0
- package/src/MarkdownText.web.tsx +668 -0
- package/src/MarkdownTextInput.tsx +42 -38
- package/src/MarkdownTextInput.web.tsx +36 -13
- package/src/atomUtils.ts +112 -0
- package/src/childrenUtils.ts +39 -0
- package/src/commonTypes.ts +69 -9
- package/src/index.tsx +2 -1
- package/src/math/environment.ts +68 -0
- package/src/math/index.ts +78 -0
- package/src/math/mathjax.ts +604 -0
- package/src/metro.ts +238 -0
- package/src/parser/index.ts +175 -10
- package/src/parser/micromark-extension-mentions.ts +104 -11
- package/src/stripUtils.ts +919 -0
- package/src/styleUtils.ts +290 -26
- package/src/web/MarkdownElements.css +67 -0
- package/src/web/MarkdownTextInput.css +9 -7
- package/src/web/utils/blockLayout.ts +124 -14
- package/src/web/utils/blockUtils.ts +365 -59
- package/src/web/utils/elementUtils.ts +87 -0
- package/src/web/utils/parserUtils.ts +119 -31
- package/src/web/utils/treeUtils.ts +6 -1
- package/src/web/utils/webStyleUtils.ts +32 -8
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +0 -6
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +0 -6
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +0 -6
- package/apple/RCTMarcusStyle+Codegen.mm +0 -64
- package/apple/RCTMarcusUtils.h +0 -26
- package/apple/RCTMarcusUtils.mm +0 -26
|
@@ -11,27 +11,50 @@ import UIKit
|
|
|
11
11
|
/// Font resolution is injected rather than reached for, so this file carries
|
|
12
12
|
/// no React dependency and can be compiled on its own. The Objective-C entry
|
|
13
13
|
/// point in `MarkdownFormatter+React.swift` supplies the production one.
|
|
14
|
+
/// `resetAttributes` distinguishes the two callers. A text input formats the
|
|
15
|
+
/// same live storage over and over, so it starts from a clean slate each
|
|
16
|
+
/// time. A paragraph is handed a string that was just built from the shadow
|
|
17
|
+
/// tree, already carrying the per-fragment attributes of every nested `Text`
|
|
18
|
+
/// -- and, where a `View` was passed as a child, an attachment that a reset
|
|
19
|
+
/// would throw away.
|
|
20
|
+
///
|
|
21
|
+
/// `display` says which of the two is being formatted, which only the list
|
|
22
|
+
/// markers care about: a display draws them -- a bullet for an unordered item,
|
|
23
|
+
/// the number at its own scale for an ordered one -- where an input shows the
|
|
24
|
+
/// marker you typed, in the base font, because there it is text being edited
|
|
25
|
+
/// rather than a rendering of it.
|
|
14
26
|
public static func format(
|
|
15
27
|
_ attributedString: NSMutableAttributedString,
|
|
16
28
|
defaultTextAttributes: [NSAttributedString.Key: Any],
|
|
17
29
|
ranges: [MarcusRange],
|
|
18
30
|
style: RCTMarcusStyle,
|
|
19
|
-
fonts: MarkdownFontProviding
|
|
31
|
+
fonts: MarkdownFontProviding,
|
|
32
|
+
resetAttributes: Bool = true,
|
|
33
|
+
display: Bool = false
|
|
20
34
|
) {
|
|
21
35
|
let fullRange = NSRange(location: 0, length: attributedString.length)
|
|
22
36
|
|
|
23
37
|
attributedString.beginEditing()
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
if resetAttributes {
|
|
40
|
+
attributedString.setAttributes(defaultTextAttributes, range: fullRange)
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
// Forces the swizzled `_textOf:equals:` into string-only comparison.
|
|
43
|
+
attributedString.addAttribute(.marcusText, value: true, range: fullRange)
|
|
44
|
+
}
|
|
29
45
|
|
|
30
46
|
// Containers arrive per line, outermost first, each preceded by the run of
|
|
31
47
|
// text its own marker takes up on that line. Laying a line out is a walk
|
|
32
48
|
// left to right: reserve a container's gutter, step over its marker, repeat.
|
|
33
|
-
var line = BlockLayout(
|
|
49
|
+
var line = BlockLayout(
|
|
50
|
+
style: style,
|
|
51
|
+
display: display,
|
|
52
|
+
baseFontSize: (defaultTextAttributes[.font] as? UIFont)?.pointSize ?? 0
|
|
53
|
+
)
|
|
34
54
|
var pendingPrefix: NSRange?
|
|
55
|
+
// Boxed once the walk is over: a block's own indent is whatever its lines
|
|
56
|
+
// ended up with, and the containers around it are still being placed here.
|
|
57
|
+
var codeBlocks: [NSRange] = []
|
|
35
58
|
|
|
36
59
|
for markdownRange in ranges {
|
|
37
60
|
if markdownRange.type == "block-prefix" {
|
|
@@ -39,6 +62,11 @@ import UIKit
|
|
|
39
62
|
continue
|
|
40
63
|
}
|
|
41
64
|
|
|
65
|
+
if markdownRange.type == "codeblock" {
|
|
66
|
+
codeBlocks.append(markdownRange.range)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
if let gutter = gutter(
|
|
43
71
|
for: markdownRange.type,
|
|
44
72
|
depth: Int(markdownRange.depth),
|
|
@@ -54,6 +82,22 @@ import UIKit
|
|
|
54
82
|
defaultTextAttributes: defaultTextAttributes
|
|
55
83
|
)
|
|
56
84
|
pendingPrefix = nil
|
|
85
|
+
|
|
86
|
+
// The text of a quoted block, as opposed to the bar beside it. Applied
|
|
87
|
+
// here rather than in `apply` because a quote is a container and so
|
|
88
|
+
// never reaches the range walk below. It covers the line's markers as
|
|
89
|
+
// well as its prose, which is why it goes on before the `syntax` ranges
|
|
90
|
+
// that arrive later and paint over it.
|
|
91
|
+
if markdownRange.type == "blockquote" {
|
|
92
|
+
applyTextStyle(
|
|
93
|
+
style.blockquote,
|
|
94
|
+
to: attributedString,
|
|
95
|
+
range: markdownRange.range,
|
|
96
|
+
defaultTextAttributes: defaultTextAttributes,
|
|
97
|
+
fonts: fonts
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
continue
|
|
58
102
|
}
|
|
59
103
|
|
|
@@ -63,15 +107,51 @@ import UIKit
|
|
|
63
107
|
range: markdownRange.range,
|
|
64
108
|
depth: Int(markdownRange.depth),
|
|
65
109
|
style: style,
|
|
110
|
+
defaultTextAttributes: defaultTextAttributes,
|
|
111
|
+
fonts: fonts
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for codeBlock in codeBlocks {
|
|
116
|
+
box(
|
|
117
|
+
codeBlock,
|
|
118
|
+
in: attributedString,
|
|
119
|
+
style: style,
|
|
120
|
+
defaultTextAttributes: defaultTextAttributes
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Last, so that a marker's own style wins over the `syntax` range that also
|
|
125
|
+
// covers it -- a display keeps that one range of markup and draws it, where
|
|
126
|
+
// everything else the colour is for has been stripped out. Only the scale
|
|
127
|
+
// reaches the width the text was indented past, so a marker style that
|
|
128
|
+
// changes the face or the size changes how the marker looks and not how much
|
|
129
|
+
// room it was given.
|
|
130
|
+
for marker in line.markerRuns {
|
|
131
|
+
applyTextStyle(
|
|
132
|
+
marker.style,
|
|
133
|
+
to: attributedString,
|
|
134
|
+
range: marker.range,
|
|
135
|
+
defaultTextAttributes: defaultTextAttributes,
|
|
66
136
|
fonts: fonts
|
|
67
137
|
)
|
|
68
138
|
}
|
|
69
139
|
|
|
70
|
-
|
|
140
|
+
let text = attributedString.string as NSString
|
|
141
|
+
|
|
142
|
+
text.enumerateSubstrings(
|
|
71
143
|
in: fullRange,
|
|
72
144
|
options: [.byLines, .substringNotRequired]
|
|
73
|
-
) { _,
|
|
145
|
+
) { _, lineRange, enclosingRange, _ in
|
|
74
146
|
applyBaselineOffset(to: attributedString, range: enclosingRange)
|
|
147
|
+
|
|
148
|
+
applyWritingDirection(
|
|
149
|
+
to: attributedString,
|
|
150
|
+
line: lineRange,
|
|
151
|
+
enclosing: enclosingRange,
|
|
152
|
+
in: text,
|
|
153
|
+
defaultTextAttributes: defaultTextAttributes
|
|
154
|
+
)
|
|
75
155
|
}
|
|
76
156
|
|
|
77
157
|
attributedString.fixAttributes(in: fullRange)
|
|
@@ -81,174 +161,695 @@ import UIKit
|
|
|
81
161
|
|
|
82
162
|
// MARK: - Per-range attributes
|
|
83
163
|
|
|
164
|
+
/// The entry a type is styled from, or nil for a type that carries no text
|
|
165
|
+
/// attributes of its own.
|
|
166
|
+
///
|
|
167
|
+
/// A fenced block's language is its own type so that something can read it,
|
|
168
|
+
/// but in an input it is part of the fence you are typing and reads as markup,
|
|
169
|
+
/// so it is styled like the rest of it.
|
|
170
|
+
private static func textStyle(
|
|
171
|
+
for type: String,
|
|
172
|
+
style: RCTMarcusStyle
|
|
173
|
+
) -> RCTMarcusTextStyle? {
|
|
174
|
+
switch type {
|
|
175
|
+
case "syntax", "language":
|
|
176
|
+
return style.syntax
|
|
177
|
+
case "link":
|
|
178
|
+
return style.link
|
|
179
|
+
case "strong":
|
|
180
|
+
return style.strong
|
|
181
|
+
case "emphasis":
|
|
182
|
+
return style.emphasis
|
|
183
|
+
case "strikethrough":
|
|
184
|
+
return style.strikethrough
|
|
185
|
+
case "heading":
|
|
186
|
+
return style.heading
|
|
187
|
+
case "code":
|
|
188
|
+
return style.code
|
|
189
|
+
case "pre":
|
|
190
|
+
return style.pre
|
|
191
|
+
case "mention":
|
|
192
|
+
return style.mention
|
|
193
|
+
default:
|
|
194
|
+
return nil
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
84
198
|
private static func apply(
|
|
85
199
|
_ type: String,
|
|
86
200
|
to attributedString: NSMutableAttributedString,
|
|
87
201
|
range: NSRange,
|
|
88
202
|
depth: Int,
|
|
89
203
|
style: RCTMarcusStyle,
|
|
204
|
+
defaultTextAttributes: [NSAttributedString.Key: Any],
|
|
90
205
|
fonts: MarkdownFontProviding
|
|
91
206
|
) {
|
|
207
|
+
if let textStyle = textStyle(for: type, style: style) {
|
|
208
|
+
applyTextStyle(
|
|
209
|
+
textStyle,
|
|
210
|
+
to: attributedString,
|
|
211
|
+
range: range,
|
|
212
|
+
// A heading's size is the one thing a style cannot state outright: level
|
|
213
|
+
// N is the base size scaled N-1 times, and the level only arrives here.
|
|
214
|
+
fontSize: type == "heading" ? headingFontSize(depth, style.heading) : nil,
|
|
215
|
+
defaultTextAttributes: defaultTextAttributes,
|
|
216
|
+
fonts: fonts
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
92
220
|
switch type {
|
|
93
|
-
case "
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
font = fonts.update(font, style: "italic")
|
|
105
|
-
case "code":
|
|
106
|
-
font = fonts.update(
|
|
107
|
-
font,
|
|
108
|
-
family: style.codeFontFamily,
|
|
109
|
-
size: style.codeFontSize,
|
|
110
|
-
weight: nil
|
|
111
|
-
)
|
|
112
|
-
case "pre":
|
|
113
|
-
font = fonts.update(
|
|
114
|
-
font,
|
|
115
|
-
family: style.preFontFamily,
|
|
116
|
-
size: style.preFontSize,
|
|
117
|
-
weight: nil
|
|
118
|
-
)
|
|
119
|
-
case "heading":
|
|
120
|
-
// Level N is the base size scaled N-1 times, so a single pair of style
|
|
121
|
-
// values covers all six.
|
|
122
|
-
var size = style.headingFontSize
|
|
123
|
-
for _ in 1..<max(depth, 1) {
|
|
124
|
-
size *= style.headingScale
|
|
125
|
-
}
|
|
126
|
-
font = fonts.update(font, family: nil, size: size, weight: "bold")
|
|
127
|
-
case "emoji":
|
|
128
|
-
font = fonts.update(
|
|
129
|
-
font,
|
|
130
|
-
family: style.emojiFontFamily,
|
|
131
|
-
size: style.emojiFontSize,
|
|
132
|
-
weight: nil
|
|
133
|
-
)
|
|
134
|
-
default:
|
|
135
|
-
break
|
|
136
|
-
}
|
|
137
|
-
attributedString.addAttribute(.font, value: font as Any, range: range)
|
|
221
|
+
case "emoji":
|
|
222
|
+
applyEmoji(style.emoji, to: attributedString, range: range, fonts: fonts)
|
|
223
|
+
|
|
224
|
+
case "code":
|
|
225
|
+
chip(range, in: attributedString, style: style.code)
|
|
226
|
+
|
|
227
|
+
// Only the text attributes: what a block is drawn on is a box behind the
|
|
228
|
+
// whole of it, applied from the `codeblock` range in box(_:in:style:).
|
|
229
|
+
case "mention":
|
|
230
|
+
chip(range, in: attributedString, style: style.mention)
|
|
231
|
+
|
|
138
232
|
default:
|
|
139
233
|
break
|
|
140
234
|
}
|
|
235
|
+
}
|
|
141
236
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
237
|
+
/// Level N is the base size scaled N-1 times, so a single pair of style values
|
|
238
|
+
/// covers all six. Zero when the style names no size, which reads as unset.
|
|
239
|
+
private static func headingFontSize(
|
|
240
|
+
_ depth: Int,
|
|
241
|
+
_ style: RCTMarcusHeadingStyle
|
|
242
|
+
) -> CGFloat? {
|
|
243
|
+
guard style.fontSize > 0 else { return nil }
|
|
149
244
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
245
|
+
var size = style.fontSize
|
|
246
|
+
for _ in 1..<max(depth, 1) {
|
|
247
|
+
size *= style.scale
|
|
248
|
+
}
|
|
249
|
+
return size
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private static func applyEmoji(
|
|
253
|
+
_ style: RCTMarcusEmojiStyle,
|
|
254
|
+
to attributedString: NSMutableAttributedString,
|
|
255
|
+
range: NSRange,
|
|
256
|
+
fonts: MarkdownFontProviding
|
|
257
|
+
) {
|
|
258
|
+
let current =
|
|
259
|
+
attributedString.attribute(.font, at: range.location, effectiveRange: nil)
|
|
260
|
+
as? UIFont
|
|
261
|
+
|
|
262
|
+
guard
|
|
263
|
+
let font = fonts.update(
|
|
264
|
+
current,
|
|
265
|
+
family: style.fontFamily,
|
|
266
|
+
size: style.fontSize > 0 ? style.fontSize : (current?.pointSize ?? 0),
|
|
267
|
+
weight: nil
|
|
155
268
|
)
|
|
269
|
+
else { return }
|
|
156
270
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
271
|
+
attributedString.addAttribute(.font, value: font, range: range)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// MARK: - Text style
|
|
275
|
+
|
|
276
|
+
/// Puts one text style on one run.
|
|
277
|
+
///
|
|
278
|
+
/// One function rather than a case per type, because every styled type now
|
|
279
|
+
/// carries the same set of attributes: what tells a mention from a heading is
|
|
280
|
+
/// the box around it and the size it is given, not which properties it
|
|
281
|
+
/// understands. `blockUtils` writes the same set as CSS, and
|
|
282
|
+
/// `MarkdownFormatter.kt` as spans.
|
|
283
|
+
///
|
|
284
|
+
/// Everything unset is left alone rather than written as a zero, so a run
|
|
285
|
+
/// inherits whatever the prose around it is drawn in.
|
|
286
|
+
private static func applyTextStyle(
|
|
287
|
+
_ style: RCTMarcusTextStyle,
|
|
288
|
+
to attributedString: NSMutableAttributedString,
|
|
289
|
+
range: NSRange,
|
|
290
|
+
fontSize: CGFloat? = nil,
|
|
291
|
+
defaultTextAttributes: [NSAttributedString.Key: Any],
|
|
292
|
+
fonts: MarkdownFontProviding
|
|
293
|
+
) {
|
|
294
|
+
guard range.length > 0, NSMaxRange(range) <= attributedString.length else {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
applyFont(
|
|
299
|
+
style,
|
|
300
|
+
to: attributedString,
|
|
301
|
+
range: range,
|
|
302
|
+
fontSize: fontSize,
|
|
303
|
+
fonts: fonts
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if let color = style.color {
|
|
307
|
+
attributedString.addAttribute(.foregroundColor, value: color, range: range)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
applyLetterSpacing(style, to: attributedString, range: range)
|
|
311
|
+
applyDecoration(style, to: attributedString, range: range)
|
|
312
|
+
applyShadow(style, to: attributedString, range: range)
|
|
313
|
+
applyLineHeight(
|
|
314
|
+
style,
|
|
315
|
+
to: attributedString,
|
|
316
|
+
range: range,
|
|
317
|
+
defaultTextAttributes: defaultTextAttributes
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// The face, the size, the weight and the slant, in that order.
|
|
322
|
+
///
|
|
323
|
+
/// The family and the size go together in one call because React Native's font
|
|
324
|
+
/// resolution takes them together, and the two traits follow so that a weight
|
|
325
|
+
/// asked for on top of a family survives the family being resolved.
|
|
326
|
+
private static func applyFont(
|
|
327
|
+
_ style: RCTMarcusTextStyle,
|
|
328
|
+
to attributedString: NSMutableAttributedString,
|
|
329
|
+
range: NSRange,
|
|
330
|
+
fontSize: CGFloat?,
|
|
331
|
+
fonts: MarkdownFontProviding
|
|
332
|
+
) {
|
|
333
|
+
let size = fontSize ?? (style.fontSize > 0 ? style.fontSize : nil)
|
|
334
|
+
|
|
335
|
+
guard
|
|
336
|
+
size != nil || style.fontFamily != nil || style.fontWeight != nil
|
|
337
|
+
|| style.fontStyle != nil
|
|
338
|
+
else { return }
|
|
339
|
+
|
|
340
|
+
var font =
|
|
341
|
+
attributedString.attribute(.font, at: range.location, effectiveRange: nil)
|
|
342
|
+
as? UIFont
|
|
343
|
+
|
|
344
|
+
if size != nil || style.fontFamily != nil {
|
|
345
|
+
font = fonts.update(
|
|
346
|
+
font,
|
|
347
|
+
family: style.fontFamily,
|
|
348
|
+
size: size ?? font?.pointSize ?? 0,
|
|
349
|
+
weight: nil
|
|
162
350
|
)
|
|
351
|
+
}
|
|
352
|
+
if let weight = style.fontWeight {
|
|
353
|
+
font = fonts.update(font, weight: weight)
|
|
354
|
+
}
|
|
355
|
+
if let fontStyle = style.fontStyle {
|
|
356
|
+
font = fonts.update(font, style: fontStyle)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
guard let font else { return }
|
|
360
|
+
|
|
361
|
+
attributedString.addAttribute(.font, value: font, range: range)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// Added to whatever each character was already kerned by rather than written
|
|
365
|
+
/// over it, because kerning is also how a marker holds a gutter open and how
|
|
366
|
+
/// an inline box holds its own room: a run given letter spacing must not
|
|
367
|
+
/// swallow the space a container reserved after the marker it starts with.
|
|
368
|
+
private static func applyLetterSpacing(
|
|
369
|
+
_ style: RCTMarcusTextStyle,
|
|
370
|
+
to attributedString: NSMutableAttributedString,
|
|
371
|
+
range: NSRange
|
|
372
|
+
) {
|
|
373
|
+
guard style.letterSpacing != 0 else { return }
|
|
374
|
+
|
|
375
|
+
var existing: [(NSRange, CGFloat)] = []
|
|
376
|
+
attributedString.enumerateAttribute(.kern, in: range, options: []) {
|
|
377
|
+
value, subrange, _ in
|
|
378
|
+
existing.append((subrange, CGFloat((value as? NSNumber)?.doubleValue ?? 0)))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (subrange, kern) in existing {
|
|
163
382
|
attributedString.addAttribute(
|
|
164
|
-
.
|
|
165
|
-
value: style.
|
|
166
|
-
range:
|
|
383
|
+
.kern,
|
|
384
|
+
value: kern + style.letterSpacing,
|
|
385
|
+
range: subrange
|
|
167
386
|
)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
168
389
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
borderRadius: style.mentionHereBorderRadius
|
|
176
|
-
)
|
|
390
|
+
private static func applyDecoration(
|
|
391
|
+
_ style: RCTMarcusTextStyle,
|
|
392
|
+
to attributedString: NSMutableAttributedString,
|
|
393
|
+
range: NSRange
|
|
394
|
+
) {
|
|
395
|
+
guard let line = style.textDecorationLine, line != "none" else { return }
|
|
177
396
|
|
|
178
|
-
|
|
179
|
-
// TODO: change mention color when it mentions current user
|
|
180
|
-
applyMention(
|
|
181
|
-
to: attributedString,
|
|
182
|
-
range: range,
|
|
183
|
-
color: style.mentionUserColor,
|
|
184
|
-
backgroundColor: style.mentionUserBackgroundColor,
|
|
185
|
-
borderRadius: style.mentionUserBorderRadius
|
|
186
|
-
)
|
|
397
|
+
let pattern = underlineStyle(style.textDecorationStyle).rawValue
|
|
187
398
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
range: range
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
borderRadius: style.mentionReportBorderRadius
|
|
195
|
-
)
|
|
399
|
+
if line.contains("underline") {
|
|
400
|
+
attributedString.addAttribute(.underlineStyle, value: pattern, range: range)
|
|
401
|
+
if let color = style.textDecorationColor {
|
|
402
|
+
attributedString.addAttribute(.underlineColor, value: color, range: range)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
196
405
|
|
|
197
|
-
|
|
406
|
+
if line.contains("line-through") {
|
|
198
407
|
attributedString.addAttribute(
|
|
199
|
-
.
|
|
200
|
-
value:
|
|
408
|
+
.strikethroughStyle,
|
|
409
|
+
value: pattern,
|
|
201
410
|
range: range
|
|
202
411
|
)
|
|
203
|
-
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
412
|
+
if let color = style.textDecorationColor {
|
|
413
|
+
attributedString.addAttribute(
|
|
414
|
+
.strikethroughColor,
|
|
415
|
+
value: color,
|
|
416
|
+
range: range
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// The CSS names for a decoration's line, as the patterns TextKit draws.
|
|
423
|
+
private static func underlineStyle(_ name: String?) -> NSUnderlineStyle {
|
|
424
|
+
switch name {
|
|
425
|
+
case "double":
|
|
426
|
+
return [.double]
|
|
427
|
+
case "dotted":
|
|
428
|
+
return [.single, .patternDot]
|
|
429
|
+
case "dashed":
|
|
430
|
+
return [.single, .patternDash]
|
|
431
|
+
default:
|
|
432
|
+
return .single
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private static func applyShadow(
|
|
437
|
+
_ style: RCTMarcusTextStyle,
|
|
438
|
+
to attributedString: NSMutableAttributedString,
|
|
439
|
+
range: NSRange
|
|
440
|
+
) {
|
|
441
|
+
guard
|
|
442
|
+
style.textShadowColor != nil || style.textShadowRadius > 0
|
|
443
|
+
|| style.textShadowOffset != .zero
|
|
444
|
+
else { return }
|
|
445
|
+
|
|
446
|
+
let shadow = NSShadow()
|
|
447
|
+
shadow.shadowColor = style.textShadowColor
|
|
448
|
+
shadow.shadowOffset = style.textShadowOffset
|
|
449
|
+
shadow.shadowBlurRadius = style.textShadowRadius
|
|
450
|
+
|
|
451
|
+
attributedString.addAttribute(.shadow, value: shadow, range: range)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// Line height is a paragraph attribute, and TextKit reads the paragraph style
|
|
455
|
+
/// of a paragraph's first character, so a run that asks for one raises the
|
|
456
|
+
/// whole line it sits on rather than only itself. Android's `LineHeightSpan`
|
|
457
|
+
/// behaves the same way, and the type declares as much.
|
|
458
|
+
private static func applyLineHeight(
|
|
459
|
+
_ style: RCTMarcusTextStyle,
|
|
460
|
+
to attributedString: NSMutableAttributedString,
|
|
461
|
+
range: NSRange,
|
|
462
|
+
defaultTextAttributes: [NSAttributedString.Key: Any]
|
|
463
|
+
) {
|
|
464
|
+
guard style.lineHeight > 0 else { return }
|
|
465
|
+
|
|
466
|
+
let paragraph = (attributedString.string as NSString).paragraphRange(for: range)
|
|
467
|
+
let indents = paragraphStyle(
|
|
468
|
+
of: attributedString,
|
|
469
|
+
at: paragraph.location,
|
|
470
|
+
defaultTextAttributes: defaultTextAttributes
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
indents.minimumLineHeight = style.lineHeight
|
|
474
|
+
indents.maximumLineHeight = style.lineHeight
|
|
475
|
+
|
|
476
|
+
attributedString.addAttribute(
|
|
477
|
+
.paragraphStyle,
|
|
478
|
+
value: indents,
|
|
479
|
+
range: paragraph
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// MARK: - Inline boxes
|
|
484
|
+
|
|
485
|
+
/// Boxes one run of text -- a mention, or an inline run of code: the room its
|
|
486
|
+
/// background takes up, and the background itself.
|
|
487
|
+
///
|
|
488
|
+
/// The room is kerning, which is the one way a line can be made to leave space
|
|
489
|
+
/// around a run of itself: a paragraph's indent would move the whole line, and
|
|
490
|
+
/// a background colour would simply sit under whatever is next to it. The
|
|
491
|
+
/// character before the run holds the space in front of it and the run's own
|
|
492
|
+
/// last character the space behind, so both sides come out of an advance
|
|
493
|
+
/// rather than out of a glyph. Only the horizontal lengths are held open that
|
|
494
|
+
/// way; vertically the box grows into the line's own spacing, because one run
|
|
495
|
+
/// cannot push the lines around it apart.
|
|
496
|
+
///
|
|
497
|
+
/// Nothing is kerned in front of a run that opens a line -- there is no
|
|
498
|
+
/// character there to widen -- and its box grows into the margin instead.
|
|
499
|
+
private static func chip(
|
|
500
|
+
_ range: NSRange,
|
|
501
|
+
in attributedString: NSMutableAttributedString,
|
|
502
|
+
style: RCTMarcusBoxStyle
|
|
503
|
+
) {
|
|
504
|
+
let background = MarkdownTextBackground()
|
|
505
|
+
background.color = style.backgroundColor ?? .clear
|
|
506
|
+
background.borderWidth = style.borderWidth
|
|
507
|
+
// A border that names no colour takes the run's own, which is the one colour
|
|
508
|
+
// the style is certain to have an opinion about.
|
|
509
|
+
background.borderColor =
|
|
510
|
+
style.borderWidth > 0 ? (style.borderColor ?? style.color) : nil
|
|
511
|
+
background.borderRadius = style.borderRadius
|
|
512
|
+
background.paddingHorizontal = style.paddingHorizontal
|
|
513
|
+
background.paddingVertical = style.paddingVertical
|
|
514
|
+
background.marginHorizontal = style.marginHorizontal
|
|
515
|
+
attributedString.addAttribute(
|
|
516
|
+
.marcusTextBackground,
|
|
517
|
+
value: background,
|
|
518
|
+
range: range
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
let gap = style.paddingHorizontal + style.marginHorizontal
|
|
522
|
+
guard gap > 0, range.length > 0 else { return }
|
|
523
|
+
|
|
524
|
+
let text = attributedString.string as NSString
|
|
525
|
+
widen(NSMaxRange(range) - 1, by: gap, in: attributedString)
|
|
526
|
+
|
|
527
|
+
let before = range.location - 1
|
|
528
|
+
if before >= 0, !isLineBreak(text.character(at: before)) {
|
|
529
|
+
widen(before, by: gap, in: attributedString)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/// Adds to whatever the character at `index` was already kerned by, so a run
|
|
534
|
+
/// that opens right after a container's marker keeps the gutter that marker
|
|
535
|
+
/// was holding open.
|
|
536
|
+
private static func widen(
|
|
537
|
+
_ index: Int,
|
|
538
|
+
by gap: CGFloat,
|
|
539
|
+
in attributedString: NSMutableAttributedString
|
|
540
|
+
) {
|
|
541
|
+
guard index >= 0, index < attributedString.length else { return }
|
|
542
|
+
|
|
543
|
+
let existing =
|
|
544
|
+
(attributedString.attribute(.kern, at: index, effectiveRange: nil)
|
|
545
|
+
as? NSNumber)?.doubleValue ?? 0
|
|
546
|
+
|
|
547
|
+
attributedString.addAttribute(
|
|
548
|
+
.kern,
|
|
549
|
+
value: CGFloat(existing) + gap,
|
|
550
|
+
range: NSRange(location: index, length: 1)
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// MARK: - Code blocks
|
|
555
|
+
|
|
556
|
+
/// Boxes one fenced or indented block: the room its background takes up, and
|
|
557
|
+
/// the background itself.
|
|
558
|
+
///
|
|
559
|
+
/// Run after the container walk rather than during it, because a block is not
|
|
560
|
+
/// a container -- its range covers every line at once, where a container
|
|
561
|
+
/// arrives one line at a time -- and because what it indents by is whatever
|
|
562
|
+
/// the containers around it left its lines at.
|
|
563
|
+
///
|
|
564
|
+
/// The padding and the margin are held open as indent on the leading edge and
|
|
565
|
+
/// as paragraph spacing above the first line and below the last, each axis
|
|
566
|
+
/// taking its own pair of lengths. Nothing is reserved on the trailing edge;
|
|
567
|
+
/// the box runs to the edge
|
|
568
|
+
/// of the text there, so a line long enough to wrap reaches the padding. A
|
|
569
|
+
/// paragraph cannot be given a trailing indent on Android at all, and a box
|
|
570
|
+
/// that stopped short on one platform only would be worse than one that
|
|
571
|
+
/// stops short on neither.
|
|
572
|
+
private static func box(
|
|
573
|
+
_ range: NSRange,
|
|
574
|
+
in attributedString: NSMutableAttributedString,
|
|
575
|
+
style: RCTMarcusStyle,
|
|
576
|
+
defaultTextAttributes: [NSAttributedString.Key: Any]
|
|
577
|
+
) {
|
|
578
|
+
let text = attributedString.string as NSString
|
|
579
|
+
guard let body = body(of: range, in: text) else { return }
|
|
580
|
+
|
|
581
|
+
// Horizontally the room is an indent, vertically it is paragraph spacing, so
|
|
582
|
+
// the two axes are held open by different things and are worked out apart.
|
|
583
|
+
let insetX = style.pre.paddingHorizontal + style.pre.marginHorizontal
|
|
584
|
+
let insetY = style.pre.paddingVertical + style.pre.marginVertical
|
|
585
|
+
var paragraphs: [NSRange] = []
|
|
586
|
+
var location = body.location
|
|
587
|
+
|
|
588
|
+
while location < NSMaxRange(body) {
|
|
589
|
+
let paragraph = text.paragraphRange(
|
|
590
|
+
for: NSRange(location: location, length: 0)
|
|
207
591
|
)
|
|
592
|
+
// A paragraph range always contains the location it was asked about, so
|
|
593
|
+
// this cannot loop; the guard is for a string that changed underneath.
|
|
594
|
+
guard paragraph.length > 0 else { break }
|
|
595
|
+
paragraphs.append(paragraph)
|
|
596
|
+
location = NSMaxRange(paragraph)
|
|
597
|
+
}
|
|
208
598
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
599
|
+
// Where the box's near edge goes: the least far in any of the block's own
|
|
600
|
+
// lines start, taken before they are moved over to make room for the
|
|
601
|
+
// padding. A line that opens with a marker starts at its marker, so the box
|
|
602
|
+
// covers that -- the `>` of a quoted block is inside the block being
|
|
603
|
+
// quoted. A head indent is measured from the edge the line begins at, so
|
|
604
|
+
// this is too, and `block.rightToLeft` is what says which edge that is.
|
|
605
|
+
var left = CGFloat.greatestFiniteMagnitude
|
|
606
|
+
|
|
607
|
+
for (index, paragraph) in paragraphs.enumerated() {
|
|
608
|
+
let indents = paragraphStyle(
|
|
609
|
+
of: attributedString,
|
|
610
|
+
at: paragraph.location,
|
|
611
|
+
defaultTextAttributes: defaultTextAttributes
|
|
214
612
|
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
613
|
+
|
|
614
|
+
left = min(left, indents.firstLineHeadIndent)
|
|
615
|
+
indents.firstLineHeadIndent += insetX
|
|
616
|
+
indents.headIndent += insetX
|
|
617
|
+
|
|
618
|
+
if index == 0 {
|
|
619
|
+
indents.paragraphSpacingBefore += insetY
|
|
620
|
+
}
|
|
621
|
+
if index == paragraphs.count - 1 {
|
|
622
|
+
indents.paragraphSpacing += insetY
|
|
623
|
+
}
|
|
624
|
+
|
|
222
625
|
attributedString.addAttribute(
|
|
223
|
-
.
|
|
224
|
-
value:
|
|
225
|
-
range:
|
|
626
|
+
.paragraphStyle,
|
|
627
|
+
value: indents,
|
|
628
|
+
range: paragraph
|
|
226
629
|
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
guard left < .greatestFiniteMagnitude else { return }
|
|
633
|
+
|
|
634
|
+
let block = MarkdownCodeBlock()
|
|
635
|
+
block.color = style.pre.backgroundColor ?? .clear
|
|
636
|
+
block.borderWidth = style.pre.borderWidth
|
|
637
|
+
block.borderColor =
|
|
638
|
+
style.pre.borderWidth > 0 ? (style.pre.borderColor ?? style.pre.color) : nil
|
|
639
|
+
block.borderRadius = style.pre.borderRadius
|
|
640
|
+
block.marginHorizontal = style.pre.marginHorizontal
|
|
641
|
+
block.marginVertical = style.pre.marginVertical
|
|
642
|
+
block.leadingInset = left + style.pre.marginHorizontal
|
|
643
|
+
// The block's own text, and all of it rather than its first line: a fence
|
|
644
|
+
// is punctuation and decides nothing, and it is present in an input and
|
|
645
|
+
// stripped from a display, so a rule that stopped at the first line would
|
|
646
|
+
// answer differently in the two. The code inside is the same either way.
|
|
647
|
+
block.rightToLeft = MarkdownWritingDirection.isRightToLeft(text, in: body)
|
|
648
|
+
|
|
649
|
+
attributedString.addAttribute(.marcusCodeBlock, value: block, range: body)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/// The lines a block actually occupies: its range with the line breaks it
|
|
653
|
+
/// opens and closes with taken off.
|
|
654
|
+
///
|
|
655
|
+
/// A display renderer removes the opening fence and leaves the break that
|
|
656
|
+
/// ended it, so a block's range there begins on the line above its first line
|
|
657
|
+
/// of code. Boxing that break would paint a box around the line above.
|
|
658
|
+
private static func body(of range: NSRange, in text: NSString) -> NSRange? {
|
|
659
|
+
var start = range.location
|
|
660
|
+
var end = NSMaxRange(range)
|
|
661
|
+
|
|
662
|
+
while start < end, isLineBreak(text.character(at: start)) {
|
|
663
|
+
start += 1
|
|
664
|
+
}
|
|
665
|
+
while end > start, isLineBreak(text.character(at: end - 1)) {
|
|
666
|
+
end -= 1
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return end > start
|
|
670
|
+
? NSRange(location: start, length: end - start)
|
|
671
|
+
: nil
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private static func isLineBreak(_ character: unichar) -> Bool {
|
|
675
|
+
character == unichar(10) || character == unichar(13)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// MARK: - List markers
|
|
679
|
+
|
|
680
|
+
/// A list marker that a display drew, and the style it is finished with once
|
|
681
|
+
/// the walk is over.
|
|
682
|
+
///
|
|
683
|
+
/// Only ordered markers are recorded. An unordered one has no glyph left to
|
|
684
|
+
/// style -- its character is drawn clear and a circle put in its advance, in
|
|
685
|
+
/// the same colour this would have applied -- and painting a foreground colour
|
|
686
|
+
/// back over it would bring the character out of hiding.
|
|
687
|
+
private struct MarkerRun {
|
|
688
|
+
let range: NSRange
|
|
689
|
+
let style: RCTMarcusTextStyle
|
|
690
|
+
}
|
|
227
691
|
|
|
692
|
+
/// Room held open either side of a list's marker, or nothing at all for a
|
|
693
|
+
/// container whose marker is shown rather than rendered.
|
|
694
|
+
private static func markerPadding(
|
|
695
|
+
for type: String,
|
|
696
|
+
style: RCTMarcusStyle,
|
|
697
|
+
display: Bool
|
|
698
|
+
) -> CGFloat {
|
|
699
|
+
guard display else { return 0 }
|
|
700
|
+
|
|
701
|
+
switch type {
|
|
702
|
+
case "list-ordered":
|
|
703
|
+
return style.orderedList.markerPadding
|
|
704
|
+
case "list-unordered":
|
|
705
|
+
return style.unorderedList.markerPadding
|
|
228
706
|
default:
|
|
229
|
-
|
|
707
|
+
return 0
|
|
230
708
|
}
|
|
231
709
|
}
|
|
232
710
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
711
|
+
/// The width a prefix's glyphs take up, drawing whatever a display draws its
|
|
712
|
+
/// marker as on the way past.
|
|
713
|
+
///
|
|
714
|
+
/// A display renders the marker rather than showing it: an unordered item's
|
|
715
|
+
/// `-` becomes a circle, an ordered item's `1.` stays the number it means but
|
|
716
|
+
/// at its own scale. Both change how wide the prefix comes out, and the
|
|
717
|
+
/// containers nested inside it start after that width, so the two are worked
|
|
718
|
+
/// out in one place.
|
|
719
|
+
private static func placeMarker(
|
|
720
|
+
_ prefix: NSRange,
|
|
721
|
+
type: String,
|
|
722
|
+
decorate: Bool,
|
|
723
|
+
in attributedString: NSMutableAttributedString,
|
|
724
|
+
style: RCTMarcusStyle,
|
|
725
|
+
display: Bool,
|
|
726
|
+
baseFontSize: CGFloat,
|
|
727
|
+
runs: inout [MarkerRun]
|
|
728
|
+
) -> CGFloat {
|
|
729
|
+
let text = attributedString.string as NSString
|
|
730
|
+
|
|
731
|
+
guard display, baseFontSize > 0, let marker = markerRun(prefix, in: text) else {
|
|
732
|
+
return measure(prefix, in: attributedString)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
switch type {
|
|
736
|
+
case "list-unordered":
|
|
737
|
+
let diameter = baseFontSize * style.unorderedList.markerScale
|
|
738
|
+
|
|
739
|
+
if decorate {
|
|
740
|
+
bullet(
|
|
741
|
+
marker,
|
|
742
|
+
diameter: diameter,
|
|
743
|
+
color: style.unorderedList.marker.color ?? .clear,
|
|
744
|
+
in: attributedString
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// The marker's own glyph is not drawn, so the circle's width stands in
|
|
749
|
+
// for it and the rest of the prefix -- an indent in front, the space
|
|
750
|
+
// behind -- measures as it always did.
|
|
751
|
+
return
|
|
752
|
+
(width(prefix, in: attributedString) - width(marker, in: attributedString)
|
|
753
|
+
+ diameter).rounded(.up)
|
|
754
|
+
|
|
755
|
+
case "list-ordered":
|
|
756
|
+
if decorate {
|
|
757
|
+
scale(
|
|
758
|
+
marker,
|
|
759
|
+
to: baseFontSize * style.orderedList.markerScale,
|
|
760
|
+
in: attributedString
|
|
761
|
+
)
|
|
762
|
+
runs.append(MarkerRun(range: marker, style: style.orderedList.marker))
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Measured after the scaling, which is on the string by now, so this is
|
|
766
|
+
// the width the number will actually be drawn at.
|
|
767
|
+
return measure(prefix, in: attributedString)
|
|
768
|
+
|
|
769
|
+
default:
|
|
770
|
+
return measure(prefix, in: attributedString)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/// The marker at the end of a prefix: the `-` of a bullet or the `1.` of a
|
|
775
|
+
/// numbered item, without the indent in front of it or the space after it.
|
|
776
|
+
///
|
|
777
|
+
/// Read off the text rather than taken from the `syntax` range covering it,
|
|
778
|
+
/// because a prefix runs from wherever the previous container's marker ended
|
|
779
|
+
/// and so can carry another container's marker with it -- the ordered list in
|
|
780
|
+
/// `- > 1. ` is handed `> 1. `. The last run of non-blanks is this
|
|
781
|
+
/// container's own.
|
|
782
|
+
private static func markerRun(_ prefix: NSRange, in text: NSString) -> NSRange? {
|
|
783
|
+
var end = NSMaxRange(prefix)
|
|
784
|
+
|
|
785
|
+
while end > prefix.location, isBlank(text.character(at: end - 1)) {
|
|
786
|
+
end -= 1
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
var start = end
|
|
790
|
+
|
|
791
|
+
while start > prefix.location, !isBlank(text.character(at: start - 1)) {
|
|
792
|
+
start -= 1
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return end > start ? NSRange(location: start, length: end - start) : nil
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/// Leaves the marker's glyph undrawn and puts a circle in the advance it had.
|
|
799
|
+
///
|
|
800
|
+
/// The character stays in the string -- it is what a reader copies and what
|
|
801
|
+
/// VoiceOver announces -- so hiding the glyph is a matter of colour, and the
|
|
802
|
+
/// kerning is what makes its advance the circle's width whatever glyph the
|
|
803
|
+
/// item happened to be written with.
|
|
804
|
+
private static func bullet(
|
|
805
|
+
_ marker: NSRange,
|
|
806
|
+
diameter: CGFloat,
|
|
236
807
|
color: UIColor,
|
|
237
|
-
|
|
238
|
-
borderRadius: CGFloat
|
|
808
|
+
in attributedString: NSMutableAttributedString
|
|
239
809
|
) {
|
|
240
|
-
|
|
810
|
+
let shape = MarkdownListBullet()
|
|
811
|
+
shape.color = color
|
|
812
|
+
shape.diameter = diameter
|
|
241
813
|
|
|
242
|
-
|
|
243
|
-
textBackground.color = backgroundColor
|
|
244
|
-
textBackground.borderRadius = borderRadius
|
|
814
|
+
attributedString.addAttribute(.marcusListBullet, value: shape, range: marker)
|
|
245
815
|
attributedString.addAttribute(
|
|
246
|
-
.
|
|
247
|
-
value:
|
|
248
|
-
range:
|
|
816
|
+
.foregroundColor,
|
|
817
|
+
value: UIColor.clear,
|
|
818
|
+
range: marker
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
widen(
|
|
822
|
+
NSMaxRange(marker) - 1,
|
|
823
|
+
by: diameter - width(marker, in: attributedString),
|
|
824
|
+
in: attributedString
|
|
249
825
|
)
|
|
250
826
|
}
|
|
251
827
|
|
|
828
|
+
/// Draws the marker at `size`, keeping the face it inherited.
|
|
829
|
+
private static func scale(
|
|
830
|
+
_ marker: NSRange,
|
|
831
|
+
to size: CGFloat,
|
|
832
|
+
in attributedString: NSMutableAttributedString
|
|
833
|
+
) {
|
|
834
|
+
guard
|
|
835
|
+
let font = attributedString.attribute(
|
|
836
|
+
.font,
|
|
837
|
+
at: marker.location,
|
|
838
|
+
effectiveRange: nil
|
|
839
|
+
) as? UIFont
|
|
840
|
+
else { return }
|
|
841
|
+
|
|
842
|
+
attributedString.addAttribute(
|
|
843
|
+
.font,
|
|
844
|
+
value: font.withSize(size),
|
|
845
|
+
range: marker
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private static func isBlank(_ character: unichar) -> Bool {
|
|
850
|
+
character == unichar(32) || character == unichar(9)
|
|
851
|
+
}
|
|
852
|
+
|
|
252
853
|
// MARK: - Block layout
|
|
253
854
|
|
|
254
855
|
/// The gutter a container reserves for itself, or nil if the type is not one.
|
|
@@ -259,13 +860,13 @@ import UIKit
|
|
|
259
860
|
) -> CGFloat? {
|
|
260
861
|
switch type {
|
|
261
862
|
case "blockquote":
|
|
262
|
-
return (style.
|
|
263
|
-
+ style.
|
|
863
|
+
return (style.blockquote.marginStart + style.blockquote.borderWidth
|
|
864
|
+
+ style.blockquote.paddingStart) * CGFloat(depth)
|
|
264
865
|
case "list-ordered":
|
|
265
|
-
return (style.
|
|
866
|
+
return (style.orderedList.marginStart + style.orderedList.paddingStart)
|
|
266
867
|
* CGFloat(depth)
|
|
267
868
|
case "list-unordered":
|
|
268
|
-
return (style.
|
|
869
|
+
return (style.unorderedList.marginStart + style.unorderedList.paddingStart)
|
|
269
870
|
* CGFloat(depth)
|
|
270
871
|
default:
|
|
271
872
|
return nil
|
|
@@ -284,8 +885,27 @@ import UIKit
|
|
|
284
885
|
/// that opened it -- otherwise its text, and any ribbon beside it, would sit
|
|
285
886
|
/// at a different offset than the line above and break the block in two.
|
|
286
887
|
private struct BlockLayout {
|
|
888
|
+
let style: RCTMarcusStyle
|
|
889
|
+
/// Whether the markers are being rendered or shown; see `format`.
|
|
890
|
+
let display: Bool
|
|
891
|
+
/// The size the wrapped `Text` draws at, which is what a marker is drawn in
|
|
892
|
+
/// proportion to. Zero when the string carries no font at all, which leaves
|
|
893
|
+
/// the markers as they were typed.
|
|
894
|
+
let baseFontSize: CGFloat
|
|
895
|
+
|
|
287
896
|
/// Last marker seen for each container type, for the lines that continue it.
|
|
288
897
|
private var markers: [String: NSRange] = [:]
|
|
898
|
+
/// The markers this walk drew, styled once it is over. See `format`.
|
|
899
|
+
private(set) var markerRuns: [MarkerRun] = []
|
|
900
|
+
|
|
901
|
+
// Spelled out because a private type's memberwise initialiser is private to
|
|
902
|
+
// the type itself.
|
|
903
|
+
init(style: RCTMarcusStyle, display: Bool, baseFontSize: CGFloat) {
|
|
904
|
+
self.style = style
|
|
905
|
+
self.display = display
|
|
906
|
+
self.baseFontSize = baseFontSize
|
|
907
|
+
}
|
|
908
|
+
|
|
289
909
|
private var lineStart = NSNotFound
|
|
290
910
|
/// Offset reached so far, from the paragraph's own left edge.
|
|
291
911
|
private var offset: CGFloat = 0
|
|
@@ -335,6 +955,16 @@ import UIKit
|
|
|
335
955
|
|
|
336
956
|
offset += gutter
|
|
337
957
|
|
|
958
|
+
// The room either side of a list marker is indent on the way in -- the
|
|
959
|
+
// marker moves right with the text -- and held open with kerning on the
|
|
960
|
+
// way out, which is the only thing that opens space after a character.
|
|
961
|
+
let marker = prefix ?? markers[type]
|
|
962
|
+
let padding =
|
|
963
|
+
marker == nil
|
|
964
|
+
? 0 : MarkdownFormatter.markerPadding(for: type, style: style, display: display)
|
|
965
|
+
|
|
966
|
+
offset += padding
|
|
967
|
+
|
|
338
968
|
if let prefix {
|
|
339
969
|
markers[type] = prefix
|
|
340
970
|
if textStart == nil {
|
|
@@ -342,8 +972,24 @@ import UIKit
|
|
|
342
972
|
}
|
|
343
973
|
}
|
|
344
974
|
|
|
345
|
-
if let marker
|
|
346
|
-
offset += MarkdownFormatter.
|
|
975
|
+
if let marker {
|
|
976
|
+
offset += MarkdownFormatter.placeMarker(
|
|
977
|
+
marker,
|
|
978
|
+
type: type,
|
|
979
|
+
// Only on the line the marker is written on: a line continuing the
|
|
980
|
+
// block reuses the one above, which has been drawn already.
|
|
981
|
+
decorate: prefix != nil,
|
|
982
|
+
in: attributedString,
|
|
983
|
+
style: style,
|
|
984
|
+
display: display,
|
|
985
|
+
baseFontSize: baseFontSize,
|
|
986
|
+
runs: &markerRuns
|
|
987
|
+
)
|
|
988
|
+
offset += padding
|
|
989
|
+
|
|
990
|
+
if prefix != nil {
|
|
991
|
+
MarkdownFormatter.pad(marker, by: padding, in: attributedString)
|
|
992
|
+
}
|
|
347
993
|
}
|
|
348
994
|
|
|
349
995
|
padded = prefix
|
|
@@ -389,6 +1035,71 @@ import UIKit
|
|
|
389
1035
|
attributedString.addAttribute(.paragraphStyle, value: style, range: range)
|
|
390
1036
|
}
|
|
391
1037
|
|
|
1038
|
+
/// Settles which way one line reads and writes the answer down twice: into
|
|
1039
|
+
/// the paragraph style, which is what moves the text and its gutter, and
|
|
1040
|
+
/// under `.marcusRightToLeft`, which is what the ribbons and boxes drawn by
|
|
1041
|
+
/// hand are placed from. Two readers, one answer, and no way for the bar to
|
|
1042
|
+
/// end up on the far side of the text from the quote it marks.
|
|
1043
|
+
///
|
|
1044
|
+
/// Writing it into the paragraph style at all is the part worth explaining.
|
|
1045
|
+
/// `NSWritingDirectionNatural` looks like it would do this by itself, and for
|
|
1046
|
+
/// the glyphs it does -- an Arabic line is laid out right to left and aligned
|
|
1047
|
+
/// to the right edge whatever the paragraph style says. What it does not do is
|
|
1048
|
+
/// turn the indents round: a head indent under a natural direction is applied
|
|
1049
|
+
/// from the left, so the quote's gutter opens on the side the text has just
|
|
1050
|
+
/// left and the text itself ends up flush against the container's right edge.
|
|
1051
|
+
/// Naming the direction is what makes `firstLineHeadIndent` mean the leading
|
|
1052
|
+
/// edge, which is what it means on the other two platforms already.
|
|
1053
|
+
///
|
|
1054
|
+
/// A direction that was named before this ran is left alone and believed: it
|
|
1055
|
+
/// came from the `writingDirection` style prop, and an app that says which way
|
|
1056
|
+
/// its text reads is not overruled by a guess about the characters in it.
|
|
1057
|
+
private static func applyWritingDirection(
|
|
1058
|
+
to attributedString: NSMutableAttributedString,
|
|
1059
|
+
line: NSRange,
|
|
1060
|
+
enclosing: NSRange,
|
|
1061
|
+
in text: NSString,
|
|
1062
|
+
defaultTextAttributes: [NSAttributedString.Key: Any]
|
|
1063
|
+
) {
|
|
1064
|
+
let declared = paragraphStyle(
|
|
1065
|
+
of: attributedString,
|
|
1066
|
+
at: enclosing.location,
|
|
1067
|
+
defaultTextAttributes: defaultTextAttributes
|
|
1068
|
+
).baseWritingDirection
|
|
1069
|
+
|
|
1070
|
+
let rightToLeft =
|
|
1071
|
+
declared == .natural
|
|
1072
|
+
? MarkdownWritingDirection.isRightToLeft(text, in: line)
|
|
1073
|
+
: declared == .rightToLeft
|
|
1074
|
+
|
|
1075
|
+
if declared == .natural {
|
|
1076
|
+
// Run by run rather than over the line in one go: a list nested inside a
|
|
1077
|
+
// quote indents its own share of the line, so one line can carry more
|
|
1078
|
+
// than one paragraph style and flattening them would lose an indent.
|
|
1079
|
+
attributedString.enumerateAttribute(
|
|
1080
|
+
.paragraphStyle,
|
|
1081
|
+
in: enclosing,
|
|
1082
|
+
options: []
|
|
1083
|
+
) { _, range, _ in
|
|
1084
|
+
let style = paragraphStyle(
|
|
1085
|
+
of: attributedString,
|
|
1086
|
+
at: range.location,
|
|
1087
|
+
defaultTextAttributes: defaultTextAttributes
|
|
1088
|
+
)
|
|
1089
|
+
style.baseWritingDirection = rightToLeft ? .rightToLeft : .leftToRight
|
|
1090
|
+
attributedString.addAttribute(.paragraphStyle, value: style, range: range)
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if rightToLeft {
|
|
1095
|
+
attributedString.addAttribute(
|
|
1096
|
+
.marcusRightToLeft,
|
|
1097
|
+
value: true,
|
|
1098
|
+
range: enclosing
|
|
1099
|
+
)
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
392
1103
|
private static func paragraphStyle(
|
|
393
1104
|
of attributedString: NSAttributedString,
|
|
394
1105
|
at location: Int,
|
|
@@ -410,6 +1121,10 @@ import UIKit
|
|
|
410
1121
|
|
|
411
1122
|
/// Opens `width` of space after `range` without touching its glyphs, so a
|
|
412
1123
|
/// nested container's gutter can sit between one marker and the next.
|
|
1124
|
+
///
|
|
1125
|
+
/// Added to whatever that character already holds: a marker padded in a
|
|
1126
|
+
/// display is the same character a nested container then pads again, and the
|
|
1127
|
+
/// second must not replace the first.
|
|
413
1128
|
private static func pad(
|
|
414
1129
|
_ range: NSRange,
|
|
415
1130
|
by width: CGFloat,
|
|
@@ -417,19 +1132,11 @@ import UIKit
|
|
|
417
1132
|
) {
|
|
418
1133
|
guard width > 0, range.length > 0 else { return }
|
|
419
1134
|
|
|
420
|
-
attributedString
|
|
421
|
-
.kern,
|
|
422
|
-
value: width,
|
|
423
|
-
range: NSRange(location: NSMaxRange(range) - 1, length: 1)
|
|
424
|
-
)
|
|
1135
|
+
widen(NSMaxRange(range) - 1, by: width, in: attributedString)
|
|
425
1136
|
}
|
|
426
1137
|
|
|
427
1138
|
/// Width `range` takes up mid-line, rounded up to a whole point.
|
|
428
1139
|
///
|
|
429
|
-
/// A sentinel is appended and its width taken back off again because `size()`
|
|
430
|
-
/// measures the range as a line of its own and drops trailing whitespace --
|
|
431
|
-
/// which a list-item prefix always ends in.
|
|
432
|
-
///
|
|
433
1140
|
/// Rounding is what keeps a quote's ribbon in one piece. TextKit snaps every
|
|
434
1141
|
/// line's origin to the device pixel grid, so a line indented past a marker
|
|
435
1142
|
/// and a line indented past nothing land on different sides of a fractional
|
|
@@ -440,6 +1147,20 @@ import UIKit
|
|
|
440
1147
|
private static func measure(
|
|
441
1148
|
_ range: NSRange,
|
|
442
1149
|
in attributedString: NSAttributedString
|
|
1150
|
+
) -> CGFloat {
|
|
1151
|
+
width(range, in: attributedString).rounded(.up)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/// The same width before it is rounded, for a caller adding to it: a marker
|
|
1155
|
+
/// that is part glyphs and part drawn shape rounds once, at the end, rather
|
|
1156
|
+
/// than once per part.
|
|
1157
|
+
///
|
|
1158
|
+
/// A sentinel is appended and its width taken back off again because `size()`
|
|
1159
|
+
/// measures the range as a line of its own and drops trailing whitespace --
|
|
1160
|
+
/// which a list-item prefix always ends in.
|
|
1161
|
+
private static func width(
|
|
1162
|
+
_ range: NSRange,
|
|
1163
|
+
in attributedString: NSAttributedString
|
|
443
1164
|
) -> CGFloat {
|
|
444
1165
|
let text = NSMutableAttributedString(
|
|
445
1166
|
attributedString: attributedString.attributedSubstring(from: range)
|
|
@@ -460,7 +1181,7 @@ import UIKit
|
|
|
460
1181
|
)
|
|
461
1182
|
text.append(sentinel)
|
|
462
1183
|
|
|
463
|
-
return
|
|
1184
|
+
return text.size().width - sentinel.size().width
|
|
464
1185
|
}
|
|
465
1186
|
|
|
466
1187
|
// MARK: - Baseline
|