@@ -124,10 +124,60 @@ public enum RenderBlockContent: Equatable {
124
124
public var code : [ String ]
125
125
/// Additional metadata for this code block.
126
126
public var metadata : RenderContentMetadata ?
127
+ /// Annotations for code blocks
128
+ public var options : CodeBlockOptions ?
129
+
130
+ /// Make a new `CodeListing` with the given data.
131
+ public init ( syntax: String ? , code: [ String ] , metadata: RenderContentMetadata ? , options: CodeBlockOptions ? ) {
132
+ self . syntax = syntax
133
+ self . code = code
134
+ self . metadata = metadata
135
+ self . options = options
136
+ }
137
+ }
138
+
139
+ public struct CodeBlockOptions : Equatable {
140
+ public var language : String ?
127
141
public var copyToClipboard : Bool
142
+ public var showLineNumbers : Bool
143
+ public var wrap : Int
144
+ public var lineAnnotations : [ LineAnnotation ]
145
+
146
+ public struct Position : Equatable , Comparable , Codable {
147
+ public static func < ( lhs: RenderBlockContent . CodeBlockOptions . Position , rhs: RenderBlockContent . CodeBlockOptions . Position ) -> Bool {
148
+ if lhs. line == rhs. line, let lhsCharacter = lhs. character, let rhsCharacter = rhs. character {
149
+ return lhsCharacter < rhsCharacter
150
+ }
151
+ return lhs. line < rhs. line
152
+ }
153
+
154
+ public init ( line: Int , character: Int ? = nil ) {
155
+ self . line = line
156
+ self . character = character
157
+ }
158
+
159
+ public var line : Int
160
+ public var character : Int ?
161
+ }
162
+
163
+ public struct LineAnnotation : Equatable , Codable {
164
+ public var style : String
165
+ public var range : Range < Position >
166
+
167
+ public init ( style: String , range: Range < Position > ) {
168
+ self . style = style
169
+ self . range = range
170
+ }
171
+ }
128
172
129
173
public enum OptionName : String , CaseIterable {
174
+ case _nonFrozenEnum_useDefaultCase
130
175
case nocopy
176
+ case wrap
177
+ case highlight
178
+ case showLineNumbers
179
+ case strikeout
180
+ case unknown
131
181
132
182
init ? ( caseInsensitive raw: some StringProtocol ) {
133
183
self . init ( rawValue: raw. lowercased ( ) )
@@ -138,12 +188,165 @@ public enum RenderBlockContent: Equatable {
138
188
Set ( OptionName . allCases. map ( \. rawValue) )
139
189
}
140
190
141
- /// Make a new `CodeListing` with the given data.
142
- public init ( syntax: String ? , code: [ String ] , metadata: RenderContentMetadata ? , copyToClipboard: Bool = FeatureFlags . current. isExperimentalCodeBlockAnnotationsEnabled) {
143
- self . syntax = syntax
144
- self . code = code
145
- self . metadata = metadata
191
+ // empty initializer with default values
192
+ public init ( ) {
193
+ self . language = " "
194
+ self . copyToClipboard = FeatureFlags . current. isExperimentalCodeBlockAnnotationsEnabled
195
+ self . showLineNumbers = false
196
+ self . wrap = 0
197
+ self . lineAnnotations = [ ]
198
+ }
199
+
200
+ public init ( parsingLanguageString language: String ? ) {
201
+ let ( lang, tokens) = Self . tokenizeLanguageString ( language)
202
+
203
+ self . language = lang
204
+ self . copyToClipboard = !tokens. contains { $0. name == . nocopy }
205
+ self . showLineNumbers = tokens. contains { $0. name == . showLineNumbers }
206
+
207
+ if let wrapString = tokens. first ( where: { $0. name == . wrap } ) ? . value,
208
+ let wrapValue = Int ( wrapString) {
209
+ self . wrap = wrapValue
210
+ } else {
211
+ self . wrap = 0
212
+ }
213
+
214
+ var annotations : [ LineAnnotation ] = [ ]
215
+
216
+ if let highlightString = tokens. first ( where: { $0. name == . highlight } ) ? . value {
217
+ let highlightValue = Self . parseCodeBlockOptionsArray ( highlightString)
218
+ for line in highlightValue {
219
+ let pos = Position ( line: line, character: nil )
220
+ let range = pos..< pos
221
+ annotations. append ( LineAnnotation ( style: " highlight " , range: range) )
222
+ }
223
+ }
224
+
225
+ if let strikeoutString = tokens. first ( where: { $0. name == . strikeout } ) ? . value {
226
+ let strikeoutValue = Self . parseCodeBlockOptionsArray ( strikeoutString)
227
+ for line in strikeoutValue {
228
+ let pos = Position ( line: line, character: nil )
229
+ let range = pos..< pos
230
+ annotations. append ( LineAnnotation ( style: " strikeout " , range: range) )
231
+ }
232
+ }
233
+
234
+ self . lineAnnotations = annotations
235
+ }
236
+
237
+ public init ( copyToClipboard: Bool = FeatureFlags . current. isExperimentalCodeBlockAnnotationsEnabled, showLineNumbers: Bool = false , wrap: Int , highlight: [ Int ] , strikeout: [ Int ] ) {
238
+ self . copyToClipboard = copyToClipboard
239
+ self . showLineNumbers = showLineNumbers
240
+ self . wrap = wrap
241
+
242
+ var annotations : [ LineAnnotation ] = [ ]
243
+ for line in highlight {
244
+ let pos = Position ( line: line, character: nil )
245
+ let range = pos..< pos
246
+ annotations. append ( LineAnnotation ( style: " highlight " , range: range) )
247
+ }
248
+ for line in strikeout {
249
+ let pos = Position ( line: line, character: nil )
250
+ let range = pos..< pos
251
+ annotations. append ( LineAnnotation ( style: " strikeout " , range: range) )
252
+ }
253
+ self . lineAnnotations = annotations
254
+ }
255
+
256
+ public init ( copyToClipboard: Bool , showLineNumbers: Bool , wrap: Int , lineAnnotations: [ LineAnnotation ] ) {
146
257
self . copyToClipboard = copyToClipboard
258
+ self . showLineNumbers = showLineNumbers
259
+ self . wrap = wrap
260
+ self . lineAnnotations = lineAnnotations
261
+ }
262
+
263
+ /// A function that parses array values on code block options from the language line string
264
+ static internal func parseCodeBlockOptionsArray( _ value: String ? ) -> [ Int ] {
265
+ guard var s = value? . trimmingCharacters ( in: . whitespaces) , !s. isEmpty else { return [ ] }
266
+
267
+ if s. hasPrefix ( " [ " ) && s. hasSuffix ( " ] " ) {
268
+ s. removeFirst ( )
269
+ s. removeLast ( )
270
+ }
271
+
272
+ return s. split ( separator: " , " ) . compactMap { Int ( $0. trimmingCharacters ( in: . whitespaces) ) }
273
+ }
274
+
275
+ /// A function that parses the language line options on code blocks, returning the language and tokens, an array of OptionName and option values
276
+ static internal func tokenizeLanguageString( _ input: String ? ) -> ( lang: String ? , tokens: [ ( name: OptionName , value: String ? ) ] ) {
277
+ guard let input else { return ( lang: nil , tokens: [ ] ) }
278
+
279
+ let parts = parseLanguageString ( input)
280
+ var tokens : [ ( OptionName , String ? ) ] = [ ]
281
+ var lang : String ? = nil
282
+
283
+ for (index, part) in parts. enumerated ( ) {
284
+ if let eq = part. firstIndex ( of: " = " ) {
285
+ let key = part [ ..< eq] . trimmingCharacters ( in: . whitespaces) . lowercased ( )
286
+ let value = part [ part. index ( after: eq) ... ] . trimmingCharacters ( in: . whitespaces)
287
+ if key == " wrap " {
288
+ tokens. append ( ( . wrap, value) )
289
+ } else if key == " highlight " {
290
+ tokens. append ( ( . highlight, value) )
291
+ } else if key == " strikeout " {
292
+ tokens. append ( ( . strikeout, value) )
293
+ } else {
294
+ tokens. append ( ( . unknown, key) )
295
+ }
296
+ } else {
297
+ let key = part. trimmingCharacters ( in: . whitespaces) . lowercased ( )
298
+ if key == " nocopy " {
299
+ tokens. append ( ( . nocopy, nil as String ? ) )
300
+ } else if key == " showlinenumbers " {
301
+ tokens. append ( ( . showLineNumbers, nil as String ? ) )
302
+ } else if key == " wrap " {
303
+ tokens. append ( ( . wrap, nil as String ? ) )
304
+ } else if key == " highlight " {
305
+ tokens. append ( ( . highlight, nil as String ? ) )
306
+ } else if key == " strikeout " {
307
+ tokens. append ( ( . strikeout, nil as String ? ) )
308
+ } else if index == 0 && !key. contains ( " [ " ) && !key. contains ( " ] " ) {
309
+ lang = key
310
+ } else {
311
+ tokens. append ( ( . unknown, key) )
312
+ }
313
+ }
314
+ }
315
+ return ( lang, tokens)
316
+ }
317
+
318
+ // helper function for tokenizeLanguageString to parse the language line
319
+ static func parseLanguageString( _ input: String ? ) -> [ Substring ] {
320
+
321
+ guard let input else { return [ ] }
322
+ var parts : [ Substring ] = [ ]
323
+ var start = input. startIndex
324
+ var i = input. startIndex
325
+
326
+ var bracketDepth = 0
327
+
328
+ while i < input. endIndex {
329
+ let c = input [ i]
330
+
331
+ if c == " [ " { bracketDepth += 1 }
332
+ else if c == " ] " { bracketDepth = max ( 0 , bracketDepth - 1 ) }
333
+ else if c == " , " && bracketDepth == 0 {
334
+ let seq = input [ start..< i]
335
+ if !seq. isEmpty {
336
+ parts. append ( seq)
337
+ }
338
+ input. formIndex ( after: & i)
339
+ start = i
340
+ continue
341
+ }
342
+ input. formIndex ( after: & i)
343
+ }
344
+ let tail = input [ start..< input. endIndex]
345
+ if !tail. isEmpty {
346
+ parts. append ( tail)
347
+ }
348
+
349
+ return parts
147
350
}
148
351
}
149
352
@@ -711,7 +914,7 @@ extension RenderBlockContent.Table: Codable {
711
914
extension RenderBlockContent : Codable {
712
915
private enum CodingKeys : CodingKey {
713
916
case type
714
- case inlineContent, content, caption, style, name, syntax, code, level, text, items, media, runtimePreview, anchor, summary, example, metadata, start, copyToClipboard
917
+ case inlineContent, content, caption, style, name, syntax, code, level, text, items, media, runtimePreview, anchor, summary, example, metadata, start, copyToClipboard, showLineNumbers , wrap , lineAnnotations
715
918
case request, response
716
919
case header, rows
717
920
case numberOfColumns, columns
@@ -734,12 +937,23 @@ extension RenderBlockContent: Codable {
734
937
self = try . aside( . init( style: style, content: container. decode ( [ RenderBlockContent ] . self, forKey: . content) ) )
735
938
case . codeListing:
736
939
let copy = FeatureFlags . current. isExperimentalCodeBlockAnnotationsEnabled
940
+ let options : CodeBlockOptions ?
941
+ if !Set( container. allKeys) . isDisjoint ( with: [ . copyToClipboard, . showLineNumbers, . wrap, . lineAnnotations] ) {
942
+ options = try CodeBlockOptions (
943
+ copyToClipboard: container. decodeIfPresent ( Bool . self, forKey: . copyToClipboard) ?? copy,
944
+ showLineNumbers: container. decodeIfPresent ( Bool . self, forKey: . showLineNumbers) ?? false ,
945
+ wrap: container. decodeIfPresent ( Int . self, forKey: . wrap) ?? 0 ,
946
+ lineAnnotations: container. decodeIfPresent ( [ CodeBlockOptions . LineAnnotation ] . self, forKey: . lineAnnotations) ?? [ ]
947
+ )
948
+ } else {
949
+ options = nil
950
+ }
737
951
self = try . codeListing( . init(
738
952
syntax: container. decodeIfPresent ( String . self, forKey: . syntax) ,
739
953
code: container. decode ( [ String ] . self, forKey: . code) ,
740
954
metadata: container. decodeIfPresent ( RenderContentMetadata . self, forKey: . metadata) ,
741
- copyToClipboard : container . decodeIfPresent ( Bool . self , forKey : . copyToClipboard ) ?? copy
742
- ) )
955
+ options : options
956
+ ) )
743
957
case . heading:
744
958
self = try . heading( . init( level: container. decode ( Int . self, forKey: . level) , text: container. decode ( String . self, forKey: . text) , anchor: container. decodeIfPresent ( String . self, forKey: . anchor) ) )
745
959
case . orderedList:
@@ -842,7 +1056,10 @@ extension RenderBlockContent: Codable {
842
1056
try container. encode ( l. syntax, forKey: . syntax)
843
1057
try container. encode ( l. code, forKey: . code)
844
1058
try container. encodeIfPresent ( l. metadata, forKey: . metadata)
845
- try container. encode ( l. copyToClipboard, forKey: . copyToClipboard)
1059
+ try container. encodeIfPresent ( l. options? . copyToClipboard, forKey: . copyToClipboard)
1060
+ try container. encodeIfPresent ( l. options? . showLineNumbers, forKey: . showLineNumbers)
1061
+ try container. encodeIfPresent ( l. options? . wrap, forKey: . wrap)
1062
+ try container. encodeIfPresent ( l. options? . lineAnnotations, forKey: . lineAnnotations)
846
1063
case . heading( let h) :
847
1064
try container. encode ( h. level, forKey: . level)
848
1065
try container. encode ( h. text, forKey: . text)
0 commit comments