@@ -124,10 +124,60 @@ public enum RenderBlockContent: Equatable {
124124 public var code : [ String ]
125125 /// Additional metadata for this code block.
126126 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 ?
127141 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+ }
128172
129173 public enum OptionName : String , CaseIterable {
174+ case _nonFrozenEnum_useDefaultCase
130175 case nocopy
176+ case wrap
177+ case highlight
178+ case showLineNumbers
179+ case strikeout
180+ case unknown
131181
132182 init ? ( caseInsensitive raw: some StringProtocol ) {
133183 self . init ( rawValue: raw. lowercased ( ) )
@@ -138,12 +188,165 @@ public enum RenderBlockContent: Equatable {
138188 Set ( OptionName . allCases. map ( \. rawValue) )
139189 }
140190
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 ] ) {
146257 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
147350 }
148351 }
149352
@@ -711,7 +914,7 @@ extension RenderBlockContent.Table: Codable {
711914extension RenderBlockContent : Codable {
712915 private enum CodingKeys : CodingKey {
713916 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
715918 case request, response
716919 case header, rows
717920 case numberOfColumns, columns
@@ -734,12 +937,23 @@ extension RenderBlockContent: Codable {
734937 self = try . aside( . init( style: style, content: container. decode ( [ RenderBlockContent ] . self, forKey: . content) ) )
735938 case . codeListing:
736939 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+ }
737951 self = try . codeListing( . init(
738952 syntax: container. decodeIfPresent ( String . self, forKey: . syntax) ,
739953 code: container. decode ( [ String ] . self, forKey: . code) ,
740954 metadata: container. decodeIfPresent ( RenderContentMetadata . self, forKey: . metadata) ,
741- copyToClipboard : container . decodeIfPresent ( Bool . self , forKey : . copyToClipboard ) ?? copy
742- ) )
955+ options : options
956+ ) )
743957 case . heading:
744958 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) ) )
745959 case . orderedList:
@@ -842,7 +1056,10 @@ extension RenderBlockContent: Codable {
8421056 try container. encode ( l. syntax, forKey: . syntax)
8431057 try container. encode ( l. code, forKey: . code)
8441058 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)
8461063 case . heading( let h) :
8471064 try container. encode ( h. level, forKey: . level)
8481065 try container. encode ( h. text, forKey: . text)
0 commit comments