@openeditor/react-native-prose-editor 0.0.8 → 0.0.10

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.
@@ -1,11 +1,4 @@
1
1
  import UIKit
2
-
3
- extension Notification.Name {
4
- static let editorImageAttachmentDidLoad = Notification.Name(
5
- "com.openeditor.editor.imageAttachmentDidLoad"
6
- )
7
- }
8
-
9
2
  enum NativeImageLoader {
10
3
  static let cache = NSCache<NSString, UIImage>()
11
4
  static let stateQueue = DispatchQueue(label: "com.openeditor.editor.image-loader-state")
@@ -99,298 +92,3 @@ enum NativeImageLoader {
99
92
  return Data(base64Encoded: payload, options: [.ignoreUnknownCharacters])
100
93
  }
101
94
  }
102
-
103
- // MARK: - Constants
104
-
105
- /// Custom NSAttributedString attribute keys for editor metadata.
106
- enum InputCoordinatorAttributes {
107
- /// Marks a character as a void element placeholder (hardBreak, horizontalRule).
108
- /// The value is the node type string (e.g. "hardBreak", "horizontalRule").
109
- static let voidNodeType = NSAttributedString.Key("com.openeditor.editor.voidNodeType")
110
-
111
- /// Stores the Rust document position (UInt32) for void elements.
112
- static let docPos = NSAttributedString.Key("com.openeditor.editor.docPos")
113
-
114
- /// Marks a character as a block boundary (for block start/end tracking).
115
- static let blockBoundary = NSAttributedString.Key("com.openeditor.editor.blockBoundary")
116
-
117
- /// Stores the block node type (e.g. "paragraph", "listItem").
118
- static let blockNodeType = NSAttributedString.Key("com.openeditor.editor.blockNodeType")
119
-
120
- /// Stores the block depth (UInt8).
121
- static let blockDepth = NSAttributedString.Key("com.openeditor.editor.blockDepth")
122
-
123
- /// Stores list context info as a dictionary for list items.
124
- static let listContext = NSAttributedString.Key("com.openeditor.editor.listContext")
125
-
126
- /// Marks blocks that should render a visible list marker.
127
- static let listMarkerContext = NSAttributedString.Key("com.openeditor.editor.listMarkerContext")
128
-
129
- /// Stores the rendered list marker color for the paragraph marker.
130
- static let listMarkerColor = NSAttributedString.Key("com.openeditor.editor.listMarkerColor")
131
-
132
- /// Stores the rendered list marker scale for unordered bullets.
133
- static let listMarkerScale = NSAttributedString.Key("com.openeditor.editor.listMarkerScale")
134
-
135
- /// Stores the paragraph base font used to render the list marker.
136
- static let listMarkerBaseFont = NSAttributedString.Key("com.openeditor.editor.listMarkerBaseFont")
137
-
138
- /// Stores the reserved list marker gutter width.
139
- static let listMarkerWidth = NSAttributedString.Key("com.openeditor.editor.listMarkerWidth")
140
-
141
- /// Stores the rendered blockquote border color.
142
- static let blockquoteBorderColor = NSAttributedString.Key("com.openeditor.editor.blockquoteBorderColor")
143
-
144
- /// Stores the rendered blockquote border width.
145
- static let blockquoteBorderWidth = NSAttributedString.Key("com.openeditor.editor.blockquoteBorderWidth")
146
-
147
- /// Stores the rendered blockquote gap between border and text.
148
- static let blockquoteMarkerGap = NSAttributedString.Key("com.openeditor.editor.blockquoteMarkerGap")
149
-
150
- /// Marks code-block paragraphs for custom background drawing.
151
- static let codeBlockBackgroundColor = NSAttributedString.Key("com.openeditor.editor.codeBlockBackgroundColor")
152
- static let codeBlockBorderRadius = NSAttributedString.Key("com.openeditor.editor.codeBlockBorderRadius")
153
- static let codeBlockPaddingHorizontal = NSAttributedString.Key("com.openeditor.editor.codeBlockPaddingHorizontal")
154
- static let codeBlockPaddingVertical = NSAttributedString.Key("com.openeditor.editor.codeBlockPaddingVertical")
155
-
156
- /// Marks synthetic zero-width placeholders used only for UIKit layout.
157
- static let syntheticPlaceholder = NSAttributedString.Key("com.openeditor.editor.syntheticPlaceholder")
158
-
159
- /// Stores the link href for visually styled link text without enabling UITextView's default link interaction.
160
- static let linkHref = NSAttributedString.Key("com.openeditor.editor.linkHref")
161
-
162
- /// Stores the owning top-level document child index for partial native patching.
163
- static let topLevelChildIndex = NSAttributedString.Key("com.openeditor.editor.topLevelChildIndex")
164
- }
165
-
166
- /// Layout constants for paragraph styles.
167
- enum LayoutConstants {
168
- /// Spacing between paragraphs (points).
169
- static let paragraphSpacing: CGFloat = 8.0
170
-
171
- /// Base indentation per depth level (points).
172
- static let indentPerDepth: CGFloat = 24.0
173
-
174
- /// Width reserved for the list bullet/number (points).
175
- static let listMarkerWidth: CGFloat = 36.0
176
-
177
- /// Gap between the list marker and the text that follows (points).
178
- static let listMarkerTextGap: CGFloat = 8.0
179
-
180
- /// Height of the horizontal rule separator line (points).
181
- static let horizontalRuleHeight: CGFloat = 1.0
182
-
183
- /// Vertical padding above and below the horizontal rule (points).
184
- static let horizontalRuleVerticalPadding: CGFloat = 8.0
185
-
186
- /// Total leading inset reserved for each blockquote depth.
187
- static let blockquoteIndent: CGFloat = 18.0
188
-
189
- /// Width of the rendered blockquote border bar.
190
- static let blockquoteBorderWidth: CGFloat = 3.0
191
-
192
- /// Gap between the blockquote border bar and the text that follows.
193
- static let blockquoteMarkerGap: CGFloat = 8.0
194
-
195
- /// Bullet character for unordered list items.
196
- static let unorderedListBullet = "\u{2022} "
197
-
198
- /// Scale factor applied only to unordered list marker glyphs.
199
- static let unorderedListMarkerFontScale: CGFloat = 2.0
200
-
201
- /// Object replacement character used for void block elements.
202
- static let objectReplacementCharacter = "\u{FFFC}"
203
- }
204
-
205
- enum NativeListMarker {
206
- static func text(for context: [String: Any]) -> String {
207
- if context["task"] as? Bool == true {
208
- return (context["checked"] as? Bool == true) ? "☑︎ " : "☐ "
209
- }
210
- if context["ordered"] as? Bool == true {
211
- let index = context["index"] as? Int ?? 1
212
- return "\(index). "
213
- }
214
- return LayoutConstants.unorderedListBullet
215
- }
216
- }
217
-
218
-
219
- // MARK: - BlockContext
220
-
221
- /// Transient context while rendering block elements. Pushed onto a stack
222
- /// when a `blockStart` element is encountered and popped on `blockEnd`.
223
- struct BlockContext {
224
- let nodeType: String
225
- let depth: UInt8
226
- var listContext: [String: Any]?
227
- var topLevelChildIndex: Int? = nil
228
- var listMarkerContext: [String: Any]? = nil
229
- var markerPending: Bool = false
230
- }
231
-
232
- // MARK: - HorizontalRuleAttachment
233
-
234
- /// NSTextAttachment subclass that draws a horizontal separator line.
235
- ///
236
- /// The attachment renders as a thin line across the available width with
237
- /// vertical padding. Used for `horizontalRule` void block elements.
238
- final class HorizontalRuleAttachment: NSTextAttachment {
239
-
240
- var lineColor: UIColor = .separator
241
- var lineHeight: CGFloat = LayoutConstants.horizontalRuleHeight
242
- var verticalPadding: CGFloat = LayoutConstants.horizontalRuleVerticalPadding
243
-
244
- override func attachmentBounds(
245
- for textContainer: NSTextContainer?,
246
- proposedLineFragment lineFrag: CGRect,
247
- glyphPosition position: CGPoint,
248
- characterIndex charIndex: Int
249
- ) -> CGRect {
250
- let totalHeight = lineHeight + (verticalPadding * 2)
251
- return CGRect(
252
- x: 0,
253
- y: 0,
254
- width: lineFrag.width,
255
- height: totalHeight
256
- )
257
- }
258
-
259
- override func image(
260
- forBounds imageBounds: CGRect,
261
- textContainer: NSTextContainer?,
262
- characterIndex charIndex: Int
263
- ) -> UIImage? {
264
- let renderer = UIGraphicsImageRenderer(bounds: imageBounds)
265
- return renderer.image { context in
266
- lineColor.setFill()
267
- let lineY = imageBounds.midY - (lineHeight / 2)
268
- let lineRect = CGRect(
269
- x: imageBounds.origin.x,
270
- y: lineY,
271
- width: imageBounds.width,
272
- height: lineHeight
273
- )
274
- context.fill(lineRect)
275
- }
276
- }
277
- }
278
-
279
- final class BlockImageAttachment: NSTextAttachment {
280
- private let source: String
281
- private let placeholderTint: UIColor
282
- private var preferredWidth: CGFloat?
283
- private var preferredHeight: CGFloat?
284
- private var loadedImage: UIImage?
285
-
286
- init(
287
- source: String,
288
- placeholderTint: UIColor,
289
- preferredWidth: CGFloat?,
290
- preferredHeight: CGFloat?
291
- ) {
292
- self.source = source
293
- self.placeholderTint = placeholderTint
294
- self.preferredWidth = preferredWidth
295
- self.preferredHeight = preferredHeight
296
- super.init(data: nil, ofType: nil)
297
- loadImageIfNeeded()
298
- }
299
-
300
- required init?(coder: NSCoder) {
301
- return nil
302
- }
303
-
304
- func setPreferredSize(width: CGFloat, height: CGFloat) {
305
- preferredWidth = width
306
- preferredHeight = height
307
- }
308
-
309
- func previewImage() -> UIImage? {
310
- loadedImage ?? image
311
- }
312
-
313
- override func attachmentBounds(
314
- for textContainer: NSTextContainer?,
315
- proposedLineFragment lineFrag: CGRect,
316
- glyphPosition position: CGPoint,
317
- characterIndex charIndex: Int
318
- ) -> CGRect {
319
- let lineFragmentWidth = lineFrag.width.isFinite ? lineFrag.width : 0
320
- let containerWidth = textContainer.map {
321
- max(0, $0.size.width - ($0.lineFragmentPadding * 2))
322
- } ?? 0
323
- let widthCandidates = [lineFragmentWidth, containerWidth].filter { $0.isFinite && $0 > 0 }
324
- let maxWidth = max(160, widthCandidates.min() ?? 160)
325
- let fallbackAspectRatio = loadedImage.flatMap { image -> CGFloat? in
326
- let imageSize = image.size
327
- guard imageSize.width > 0, imageSize.height > 0 else { return nil }
328
- return imageSize.height / imageSize.width
329
- } ?? 0.56
330
-
331
- var resolvedWidth = preferredWidth
332
- var resolvedHeight = preferredHeight
333
-
334
- if resolvedWidth == nil, resolvedHeight == nil, let loadedImage {
335
- let imageSize = loadedImage.size
336
- if imageSize.width > 0, imageSize.height > 0 {
337
- resolvedWidth = imageSize.width
338
- resolvedHeight = imageSize.height
339
- }
340
- } else if resolvedWidth == nil, let resolvedHeight {
341
- resolvedWidth = resolvedHeight / fallbackAspectRatio
342
- } else if resolvedHeight == nil, let resolvedWidth {
343
- resolvedHeight = resolvedWidth * fallbackAspectRatio
344
- }
345
-
346
- let width = max(1, resolvedWidth ?? maxWidth)
347
- let height = max(1, resolvedHeight ?? min(180, maxWidth * fallbackAspectRatio))
348
- let scale = min(1, maxWidth / width)
349
- return CGRect(x: 0, y: 0, width: width * scale, height: height * scale)
350
- }
351
-
352
- override func image(
353
- forBounds imageBounds: CGRect,
354
- textContainer: NSTextContainer?,
355
- characterIndex charIndex: Int
356
- ) -> UIImage? {
357
- if let loadedImage {
358
- return loadedImage
359
- }
360
-
361
- let renderer = UIGraphicsImageRenderer(bounds: imageBounds)
362
- return renderer.image { _ in
363
- let path = UIBezierPath(roundedRect: imageBounds, cornerRadius: 12)
364
- UIColor.secondarySystemFill.setFill()
365
- path.fill()
366
-
367
- let iconSize = min(imageBounds.width, imageBounds.height) * 0.28
368
- let iconOrigin = CGPoint(
369
- x: imageBounds.midX - (iconSize / 2),
370
- y: imageBounds.midY - (iconSize / 2)
371
- )
372
- let iconRect = CGRect(origin: iconOrigin, size: CGSize(width: iconSize, height: iconSize))
373
-
374
- if #available(iOS 13.0, *) {
375
- let config = UIImage.SymbolConfiguration(pointSize: iconSize, weight: .medium)
376
- let icon = UIImage(systemName: "photo", withConfiguration: config)?
377
- .withTintColor(placeholderTint.withAlphaComponent(0.7), renderingMode: .alwaysOriginal)
378
- icon?.draw(in: iconRect)
379
- }
380
- }
381
- }
382
-
383
- private func loadImageIfNeeded() {
384
- NativeImageLoader.load(source: source) { [weak self] image in
385
- guard let self,
386
- let image
387
- else {
388
- return
389
- }
390
- self.loadedImage = image
391
- self.image = image
392
- NotificationCenter.default.post(name: .editorImageAttachmentDidLoad, object: self)
393
- }
394
- }
395
-
396
- }