@apollohg/react-native-prose-editor 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +143 -0
  3. package/android/build.gradle +39 -0
  4. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  5. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  6. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
  7. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
  8. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
  10. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
  11. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
  12. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
  13. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
  14. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
  15. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
  16. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
  17. package/expo-module.config.json +9 -0
  18. package/ios/EditorAddons.swift +228 -0
  19. package/ios/EditorCore.xcframework/Info.plist +44 -0
  20. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  21. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  22. package/ios/EditorLayoutManager.swift +254 -0
  23. package/ios/EditorTheme.swift +372 -0
  24. package/ios/Generated_editor_core.swift +1143 -0
  25. package/ios/NativeEditorExpoView.swift +1417 -0
  26. package/ios/NativeEditorModule.swift +263 -0
  27. package/ios/PositionBridge.swift +278 -0
  28. package/ios/ReactNativeProseEditor.podspec +49 -0
  29. package/ios/RenderBridge.swift +825 -0
  30. package/ios/RichTextEditorView.swift +1559 -0
  31. package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
  32. package/ios/editor_coreFFI/module.modulemap +7 -0
  33. package/ios/editor_coreFFI.h +904 -0
  34. package/ios/editor_coreFFI.modulemap +7 -0
  35. package/package.json +66 -0
  36. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  37. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  38. package/rust/android/x86_64/libeditor_core.so +0 -0
  39. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2014 -0
  40. package/src/EditorTheme.ts +130 -0
  41. package/src/EditorToolbar.tsx +620 -0
  42. package/src/NativeEditorBridge.ts +607 -0
  43. package/src/NativeRichTextEditor.tsx +951 -0
  44. package/src/addons.ts +158 -0
  45. package/src/index.ts +63 -0
  46. package/src/schemas.ts +153 -0
  47. package/src/useNativeEditor.ts +173 -0
@@ -0,0 +1,211 @@
1
+ package com.apollohg.editor
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.drawable.GradientDrawable
6
+ import android.util.AttributeSet
7
+ import android.view.MotionEvent
8
+ import android.view.ViewGroup
9
+ import android.widget.FrameLayout
10
+ import android.widget.LinearLayout
11
+ import android.widget.ScrollView
12
+ import uniffi.editor_core.*
13
+
14
+ /** Container view that owns the native editor text field. */
15
+ class RichTextEditorView @JvmOverloads constructor(
16
+ context: Context,
17
+ attrs: AttributeSet? = null,
18
+ defStyleAttr: Int = 0
19
+ ) : LinearLayout(context, attrs, defStyleAttr) {
20
+
21
+ private class EditorScrollView(context: Context) : ScrollView(context) {
22
+ private fun updateParentIntercept(action: Int) {
23
+ val canScroll = canScrollVertically(-1) || canScrollVertically(1)
24
+ if (!canScroll) return
25
+ when (action) {
26
+ MotionEvent.ACTION_DOWN,
27
+ MotionEvent.ACTION_MOVE -> parent?.requestDisallowInterceptTouchEvent(true)
28
+ MotionEvent.ACTION_UP,
29
+ MotionEvent.ACTION_CANCEL -> parent?.requestDisallowInterceptTouchEvent(false)
30
+ }
31
+ }
32
+
33
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
34
+ updateParentIntercept(ev.actionMasked)
35
+ return super.onInterceptTouchEvent(ev)
36
+ }
37
+
38
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
39
+ updateParentIntercept(ev.actionMasked)
40
+ return super.onTouchEvent(ev)
41
+ }
42
+ }
43
+
44
+ val editorEditText: EditorEditText
45
+ val editorScrollView: ScrollView
46
+
47
+ private var heightBehavior: EditorHeightBehavior = EditorHeightBehavior.FIXED
48
+ private var theme: EditorTheme? = null
49
+ private var baseBackgroundColor: Int = Color.WHITE
50
+ private var viewportBottomInsetPx: Int = 0
51
+ internal var appliedCornerRadiusPx: Float = 0f
52
+
53
+ /** Binds or unbinds the Rust editor instance. */
54
+ var editorId: Long = 0
55
+ set(value) {
56
+ field = value
57
+ if (value != 0L) {
58
+ editorEditText.bindEditor(value)
59
+ } else {
60
+ editorEditText.unbindEditor()
61
+ }
62
+ }
63
+
64
+ init {
65
+ orientation = VERTICAL
66
+
67
+ editorEditText = EditorEditText(context)
68
+ editorScrollView = EditorScrollView(context).apply {
69
+ clipToPadding = false
70
+ isFillViewport = false
71
+ }
72
+ editorScrollView.addView(editorEditText, createEditorLayoutParams())
73
+
74
+ addView(editorScrollView, createContainerLayoutParams())
75
+ updateScrollContainerAppearance()
76
+ updateScrollContainerInsets()
77
+ }
78
+
79
+ fun configure(
80
+ textSizePx: Float = 16f * resources.displayMetrics.density,
81
+ textColor: Int = Color.BLACK,
82
+ backgroundColor: Int = Color.WHITE
83
+ ) {
84
+ baseBackgroundColor = backgroundColor
85
+ editorEditText.setBaseStyle(textSizePx, textColor, backgroundColor)
86
+ updateScrollContainerAppearance()
87
+ }
88
+
89
+ fun applyTheme(theme: EditorTheme?) {
90
+ this.theme = theme
91
+ val previousScrollY = editorScrollView.scrollY
92
+ editorEditText.applyTheme(theme)
93
+ updateScrollContainerAppearance()
94
+ updateScrollContainerInsets()
95
+ if (heightBehavior == EditorHeightBehavior.FIXED) {
96
+ editorScrollView.post {
97
+ val childHeight = editorScrollView.getChildAt(0)?.height ?: 0
98
+ val maxScrollY = maxOf(
99
+ 0,
100
+ childHeight + editorScrollView.paddingTop + editorScrollView.paddingBottom - editorScrollView.height
101
+ )
102
+ editorScrollView.scrollTo(0, previousScrollY.coerceIn(0, maxScrollY))
103
+ }
104
+ }
105
+ }
106
+
107
+ fun setHeightBehavior(heightBehavior: EditorHeightBehavior) {
108
+ if (this.heightBehavior == heightBehavior) return
109
+ this.heightBehavior = heightBehavior
110
+ editorEditText.setHeightBehavior(heightBehavior)
111
+ editorEditText.layoutParams = createEditorLayoutParams()
112
+ editorScrollView.layoutParams = createContainerLayoutParams()
113
+ editorScrollView.isVerticalScrollBarEnabled = heightBehavior == EditorHeightBehavior.FIXED
114
+ editorScrollView.overScrollMode = if (heightBehavior == EditorHeightBehavior.FIXED) {
115
+ OVER_SCROLL_IF_CONTENT_SCROLLS
116
+ } else {
117
+ OVER_SCROLL_NEVER
118
+ }
119
+ updateScrollContainerInsets()
120
+ requestLayout()
121
+ }
122
+
123
+ fun setViewportBottomInsetPx(bottomInsetPx: Int) {
124
+ val clampedInset = bottomInsetPx.coerceAtLeast(0)
125
+ if (viewportBottomInsetPx == clampedInset) return
126
+ viewportBottomInsetPx = clampedInset
127
+ updateScrollContainerInsets()
128
+ editorEditText.setViewportBottomInsetPx(clampedInset)
129
+ requestLayout()
130
+ }
131
+
132
+ fun setContent(html: String) {
133
+ if (editorId == 0L) return
134
+ val renderJSON = editorSetHtml(editorId.toULong(), html)
135
+ editorEditText.applyRenderJSON(renderJSON)
136
+ }
137
+
138
+ fun setContent(json: org.json.JSONObject) {
139
+ if (editorId == 0L) return
140
+ val renderJSON = editorSetJson(editorId.toULong(), json.toString())
141
+ editorEditText.applyRenderJSON(renderJSON)
142
+ }
143
+
144
+ override fun onDetachedFromWindow() {
145
+ super.onDetachedFromWindow()
146
+ if (editorId != 0L) {
147
+ editorEditText.unbindEditor()
148
+ }
149
+ }
150
+
151
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
152
+ if (heightBehavior != EditorHeightBehavior.AUTO_GROW) {
153
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
154
+ return
155
+ }
156
+
157
+ val childWidthSpec = getChildMeasureSpec(
158
+ widthMeasureSpec,
159
+ paddingLeft + paddingRight,
160
+ editorScrollView.layoutParams.width
161
+ )
162
+ val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
163
+ editorScrollView.measure(childWidthSpec, childHeightSpec)
164
+
165
+ val measuredWidth = resolveSize(
166
+ editorScrollView.measuredWidth + paddingLeft + paddingRight,
167
+ widthMeasureSpec
168
+ )
169
+ val desiredHeight = editorScrollView.measuredHeight + paddingTop + paddingBottom
170
+ val measuredHeight = when (MeasureSpec.getMode(heightMeasureSpec)) {
171
+ MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
172
+ else -> desiredHeight
173
+ }
174
+ setMeasuredDimension(measuredWidth, measuredHeight)
175
+ }
176
+
177
+ private fun updateScrollContainerAppearance() {
178
+ val cornerRadiusPx = (theme?.borderRadius ?: 0f) * resources.displayMetrics.density
179
+ editorScrollView.background = GradientDrawable().apply {
180
+ cornerRadius = cornerRadiusPx
181
+ setColor(theme?.backgroundColor ?: baseBackgroundColor)
182
+ }
183
+ editorScrollView.clipToOutline = cornerRadiusPx > 0f
184
+ appliedCornerRadiusPx = cornerRadiusPx
185
+ }
186
+
187
+ private fun updateScrollContainerInsets() {
188
+ if (heightBehavior != EditorHeightBehavior.FIXED) {
189
+ editorScrollView.setPadding(0, 0, 0, 0)
190
+ return
191
+ }
192
+
193
+ val density = resources.displayMetrics.density
194
+ val topInset = ((theme?.contentInsets?.top ?: 0f) * density).toInt()
195
+ val bottomInset = ((theme?.contentInsets?.bottom ?: 0f) * density).toInt()
196
+ editorScrollView.setPadding(0, topInset, 0, bottomInset + viewportBottomInsetPx)
197
+ }
198
+
199
+ private fun createContainerLayoutParams(): LayoutParams =
200
+ if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
201
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
202
+ } else {
203
+ LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)
204
+ }
205
+
206
+ private fun createEditorLayoutParams(): FrameLayout.LayoutParams =
207
+ FrameLayout.LayoutParams(
208
+ ViewGroup.LayoutParams.MATCH_PARENT,
209
+ ViewGroup.LayoutParams.WRAP_CONTENT
210
+ )
211
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "ios": {
4
+ "modules": ["NativeEditorModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["com.apollohg.editor.NativeEditorModule"]
8
+ }
9
+ }
@@ -0,0 +1,228 @@
1
+ import UIKit
2
+
3
+ struct NativeMentionSuggestion {
4
+ let key: String
5
+ let title: String
6
+ let subtitle: String?
7
+ let label: String
8
+ let attrs: [String: Any]
9
+
10
+ init?(dictionary: [String: Any]) {
11
+ guard let key = dictionary["key"] as? String,
12
+ let title = dictionary["title"] as? String,
13
+ let label = dictionary["label"] as? String
14
+ else {
15
+ return nil
16
+ }
17
+
18
+ self.key = key
19
+ self.title = title
20
+ self.subtitle = dictionary["subtitle"] as? String
21
+ self.label = label
22
+ self.attrs = dictionary["attrs"] as? [String: Any] ?? [:]
23
+ }
24
+ }
25
+
26
+ struct NativeMentionsAddonConfig {
27
+ let trigger: String
28
+ let suggestions: [NativeMentionSuggestion]
29
+ let theme: EditorMentionTheme?
30
+
31
+ init?(dictionary: [String: Any]) {
32
+ let trigger = (dictionary["trigger"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
33
+ self.trigger = (trigger?.isEmpty == false ? trigger : "@") ?? "@"
34
+ self.suggestions = ((dictionary["suggestions"] as? [[String: Any]]) ?? []).compactMap(NativeMentionSuggestion.init(dictionary:))
35
+ if let theme = dictionary["theme"] as? [String: Any] {
36
+ self.theme = EditorMentionTheme(dictionary: theme)
37
+ } else {
38
+ self.theme = nil
39
+ }
40
+ }
41
+ }
42
+
43
+ struct NativeEditorAddons {
44
+ let mentions: NativeMentionsAddonConfig?
45
+
46
+ static func from(json: String?) -> NativeEditorAddons {
47
+ guard let json,
48
+ let data = json.data(using: .utf8),
49
+ let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
50
+ else {
51
+ return NativeEditorAddons(mentions: nil)
52
+ }
53
+
54
+ return NativeEditorAddons(
55
+ mentions: (raw["mentions"] as? [String: Any]).flatMap(NativeMentionsAddonConfig.init(dictionary:))
56
+ )
57
+ }
58
+ }
59
+
60
+ struct MentionQueryState: Equatable {
61
+ let query: String
62
+ let trigger: String
63
+ let anchor: UInt32
64
+ let head: UInt32
65
+ }
66
+
67
+ func isMentionIdentifierScalar(_ scalar: Unicode.Scalar) -> Bool {
68
+ CharacterSet.alphanumerics.contains(scalar) || scalar == "_" || scalar == "-"
69
+ }
70
+
71
+ func resolveMentionQueryState(
72
+ in text: String,
73
+ cursorScalar: UInt32,
74
+ trigger: String,
75
+ isCaretInsideMention: Bool
76
+ ) -> MentionQueryState? {
77
+ guard !isCaretInsideMention else { return nil }
78
+
79
+ let scalars = Array(text.unicodeScalars)
80
+ let scalarCount = UInt32(scalars.count)
81
+ guard cursorScalar <= scalarCount else { return nil }
82
+
83
+ let triggerScalars = Array(trigger.unicodeScalars)
84
+ guard triggerScalars.count == 1, let triggerScalar = triggerScalars.first else {
85
+ return nil
86
+ }
87
+
88
+ var start = Int(cursorScalar)
89
+ while start > 0 {
90
+ let previous = scalars[start - 1]
91
+ if previous.properties.isWhitespace
92
+ || previous == "\n"
93
+ || previous == "\u{FFFC}"
94
+ || (!isMentionIdentifierScalar(previous) && previous != triggerScalar)
95
+ {
96
+ break
97
+ }
98
+ start -= 1
99
+ }
100
+
101
+ guard start < scalars.count, scalars[start] == triggerScalar else {
102
+ return nil
103
+ }
104
+ if start > 0 {
105
+ let previous = scalars[start - 1]
106
+ if isMentionIdentifierScalar(previous) {
107
+ return nil
108
+ }
109
+ }
110
+
111
+ let queryStart = start + 1
112
+ let cursor = Int(cursorScalar)
113
+ guard queryStart <= cursor else { return nil }
114
+ let query = String(String.UnicodeScalarView(scalars[queryStart..<cursor]))
115
+ if query.unicodeScalars.contains(where: {
116
+ $0.properties.isWhitespace || $0 == "\n" || $0 == "\u{FFFC}"
117
+ }) {
118
+ return nil
119
+ }
120
+
121
+ return MentionQueryState(
122
+ query: query,
123
+ trigger: trigger,
124
+ anchor: UInt32(start),
125
+ head: cursorScalar
126
+ )
127
+ }
128
+
129
+ final class MentionSuggestionChipButton: UIButton {
130
+ private let titleLabelView = UILabel()
131
+ private let subtitleLabelView = UILabel()
132
+ private let stackView = UIStackView()
133
+ private var theme: EditorMentionTheme?
134
+
135
+ let suggestion: NativeMentionSuggestion
136
+
137
+ init(suggestion: NativeMentionSuggestion, theme: EditorMentionTheme?) {
138
+ self.suggestion = suggestion
139
+ self.theme = theme
140
+ super.init(frame: .zero)
141
+ translatesAutoresizingMaskIntoConstraints = false
142
+ layer.cornerRadius = 12
143
+ clipsToBounds = true
144
+ if #available(iOS 15.0, *) {
145
+ var configuration = UIButton.Configuration.plain()
146
+ configuration.contentInsets = .zero
147
+ self.configuration = configuration
148
+ }
149
+
150
+ titleLabelView.translatesAutoresizingMaskIntoConstraints = false
151
+ titleLabelView.isUserInteractionEnabled = false
152
+ titleLabelView.font = .systemFont(ofSize: 14, weight: .semibold)
153
+ titleLabelView.text = suggestion.label
154
+ titleLabelView.numberOfLines = 1
155
+
156
+ subtitleLabelView.translatesAutoresizingMaskIntoConstraints = false
157
+ subtitleLabelView.isUserInteractionEnabled = false
158
+ subtitleLabelView.font = .systemFont(ofSize: 12)
159
+ subtitleLabelView.text = suggestion.subtitle
160
+ subtitleLabelView.numberOfLines = 1
161
+ subtitleLabelView.isHidden = suggestion.subtitle == nil
162
+
163
+ stackView.translatesAutoresizingMaskIntoConstraints = false
164
+ stackView.isUserInteractionEnabled = false
165
+ stackView.axis = .vertical
166
+ stackView.alignment = .fill
167
+ stackView.spacing = 1
168
+ stackView.addArrangedSubview(titleLabelView)
169
+ stackView.addArrangedSubview(subtitleLabelView)
170
+ addSubview(stackView)
171
+
172
+ NSLayoutConstraint.activate([
173
+ stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
174
+ stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
175
+ stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
176
+ stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
177
+ heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
178
+ ])
179
+
180
+ addTarget(self, action: #selector(handleTouchDown), for: [.touchDown, .touchDragEnter])
181
+ addTarget(self, action: #selector(handleTouchUp), for: [.touchCancel, .touchDragExit, .touchUpInside, .touchUpOutside])
182
+ apply(theme: theme)
183
+ updateAppearance(highlighted: false)
184
+ }
185
+
186
+ required init?(coder: NSCoder) {
187
+ return nil
188
+ }
189
+
190
+ func apply(theme: EditorMentionTheme?) {
191
+ self.theme = theme
192
+ layer.cornerRadius = theme?.borderRadius ?? 12
193
+ layer.borderColor = (theme?.borderColor ?? UIColor.clear).cgColor
194
+ layer.borderWidth = theme?.borderWidth ?? 0
195
+ subtitleLabelView.isHidden = suggestion.subtitle == nil
196
+ updateAppearance(highlighted: isHighlighted)
197
+ }
198
+
199
+ override var isHighlighted: Bool {
200
+ didSet {
201
+ updateAppearance(highlighted: isHighlighted)
202
+ }
203
+ }
204
+
205
+ @objc private func handleTouchDown() {
206
+ updateAppearance(highlighted: true)
207
+ }
208
+
209
+ @objc private func handleTouchUp() {
210
+ updateAppearance(highlighted: false)
211
+ }
212
+
213
+ private func updateAppearance(highlighted: Bool) {
214
+ backgroundColor = highlighted
215
+ ? (theme?.optionHighlightedBackgroundColor ?? UIColor.systemBlue.withAlphaComponent(0.12))
216
+ : (theme?.backgroundColor ?? UIColor.secondarySystemBackground)
217
+ titleLabelView.textColor = highlighted
218
+ ? (theme?.optionHighlightedTextColor ?? theme?.optionTextColor ?? .label)
219
+ : (theme?.optionTextColor ?? theme?.textColor ?? .label)
220
+ subtitleLabelView.textColor = theme?.optionSecondaryTextColor ?? .secondaryLabel
221
+ }
222
+
223
+ func contentViewsAllowTouchPassthroughForTesting() -> Bool {
224
+ !stackView.isUserInteractionEnabled
225
+ && !titleLabelView.isUserInteractionEnabled
226
+ && !subtitleLabelView.isUserInteractionEnabled
227
+ }
228
+ }
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>AvailableLibraries</key>
6
+ <array>
7
+ <dict>
8
+ <key>BinaryPath</key>
9
+ <string>libeditor_core.a</string>
10
+ <key>LibraryIdentifier</key>
11
+ <string>ios-arm64_x86_64-simulator</string>
12
+ <key>LibraryPath</key>
13
+ <string>libeditor_core.a</string>
14
+ <key>SupportedArchitectures</key>
15
+ <array>
16
+ <string>arm64</string>
17
+ <string>x86_64</string>
18
+ </array>
19
+ <key>SupportedPlatform</key>
20
+ <string>ios</string>
21
+ <key>SupportedPlatformVariant</key>
22
+ <string>simulator</string>
23
+ </dict>
24
+ <dict>
25
+ <key>BinaryPath</key>
26
+ <string>libeditor_core.a</string>
27
+ <key>LibraryIdentifier</key>
28
+ <string>ios-arm64</string>
29
+ <key>LibraryPath</key>
30
+ <string>libeditor_core.a</string>
31
+ <key>SupportedArchitectures</key>
32
+ <array>
33
+ <string>arm64</string>
34
+ </array>
35
+ <key>SupportedPlatform</key>
36
+ <string>ios</string>
37
+ </dict>
38
+ </array>
39
+ <key>CFBundlePackageType</key>
40
+ <string>XFWK</string>
41
+ <key>XCFrameworkFormatVersion</key>
42
+ <string>1.0</string>
43
+ </dict>
44
+ </plist>