-
Notifications
You must be signed in to change notification settings - Fork 158
Add experimental code block options: highlight, strikeout, wrap, showLineNumbers #1287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f90d7a
2e570f9
33d929c
9315bdb
20b0c08
73c7416
0a7f55f
01195f9
f3c9328
5a6b443
1d25949
81f26eb
fccebb7
456ba3a
d2f3b06
52212eb
0e010ac
8be4df3
83737dd
12960a0
9649d27
38f4ef3
6b0956c
0c57de0
8028002
074081a
48d1c46
0d80729
45c6592
4cb5248
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,10 +124,60 @@ public enum RenderBlockContent: Equatable { | |
public var code: [String] | ||
/// Additional metadata for this code block. | ||
public var metadata: RenderContentMetadata? | ||
/// Annotations for code blocks | ||
public var options: CodeBlockOptions? | ||
|
||
/// Make a new `CodeListing` with the given data. | ||
public init(syntax: String?, code: [String], metadata: RenderContentMetadata?, options: CodeBlockOptions?) { | ||
self.syntax = syntax | ||
self.code = code | ||
self.metadata = metadata | ||
self.options = options | ||
} | ||
} | ||
|
||
public struct CodeBlockOptions: Equatable { | ||
public var language: String? | ||
public var copyToClipboard: Bool | ||
public var showLineNumbers: Bool | ||
public var wrap: Int | ||
public var lineAnnotations: [LineAnnotation] | ||
|
||
public struct Position: Equatable, Comparable, Codable { | ||
public static func < (lhs: RenderBlockContent.CodeBlockOptions.Position, rhs: RenderBlockContent.CodeBlockOptions.Position) -> Bool { | ||
if lhs.line == rhs.line, let lhsCharacter = lhs.character, let rhsCharacter = rhs.character { | ||
return lhsCharacter < rhsCharacter | ||
} | ||
return lhs.line < rhs.line | ||
} | ||
|
||
public init(line: Int, character: Int? = nil) { | ||
self.line = line | ||
self.character = character | ||
} | ||
|
||
public var line: Int | ||
public var character: Int? | ||
} | ||
|
||
public struct LineAnnotation: Equatable, Codable { | ||
public var style: String | ||
public var range: Range<Position> | ||
|
||
public init(style: String, range: Range<Position>) { | ||
self.style = style | ||
self.range = range | ||
} | ||
} | ||
|
||
public enum OptionName: String, CaseIterable { | ||
DebugSteven marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case _nonFrozenEnum_useDefaultCase | ||
case nocopy | ||
case wrap | ||
case highlight | ||
case showLineNumbers | ||
case strikeout | ||
case unknown | ||
|
||
init?(caseInsensitive raw: some StringProtocol) { | ||
self.init(rawValue: raw.lowercased()) | ||
|
@@ -138,12 +188,165 @@ public enum RenderBlockContent: Equatable { | |
Set(OptionName.allCases.map(\.rawValue)) | ||
} | ||
|
||
/// Make a new `CodeListing` with the given data. | ||
public init(syntax: String?, code: [String], metadata: RenderContentMetadata?, copyToClipboard: Bool = FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled) { | ||
self.syntax = syntax | ||
self.code = code | ||
self.metadata = metadata | ||
// empty initializer with default values | ||
public init() { | ||
self.language = "" | ||
self.copyToClipboard = FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled | ||
self.showLineNumbers = false | ||
self.wrap = 0 | ||
self.lineAnnotations = [] | ||
} | ||
|
||
public init(parsingLanguageString language: String?) { | ||
let (lang, tokens) = Self.tokenizeLanguageString(language) | ||
|
||
self.language = lang | ||
self.copyToClipboard = !tokens.contains { $0.name == .nocopy } | ||
self.showLineNumbers = tokens.contains { $0.name == .showLineNumbers } | ||
|
||
if let wrapString = tokens.first(where: { $0.name == .wrap })?.value, | ||
let wrapValue = Int(wrapString) { | ||
self.wrap = wrapValue | ||
} else { | ||
self.wrap = 0 | ||
} | ||
|
||
var annotations: [LineAnnotation] = [] | ||
|
||
if let highlightString = tokens.first(where: { $0.name == .highlight })?.value { | ||
let highlightValue = Self.parseCodeBlockOptionsArray(highlightString) | ||
for line in highlightValue { | ||
let pos = Position(line: line, character: nil) | ||
let range = pos..<pos | ||
annotations.append(LineAnnotation(style: "highlight", range: range)) | ||
} | ||
} | ||
|
||
if let strikeoutString = tokens.first(where: { $0.name == .strikeout })?.value { | ||
let strikeoutValue = Self.parseCodeBlockOptionsArray(strikeoutString) | ||
for line in strikeoutValue { | ||
let pos = Position(line: line, character: nil) | ||
let range = pos..<pos | ||
annotations.append(LineAnnotation(style: "strikeout", range: range)) | ||
} | ||
} | ||
|
||
self.lineAnnotations = annotations | ||
} | ||
|
||
public init(copyToClipboard: Bool = FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled, showLineNumbers: Bool = false, wrap: Int, highlight: [Int], strikeout: [Int]) { | ||
self.copyToClipboard = copyToClipboard | ||
self.showLineNumbers = showLineNumbers | ||
self.wrap = wrap | ||
|
||
var annotations: [LineAnnotation] = [] | ||
for line in highlight { | ||
let pos = Position(line: line, character: nil) | ||
let range = pos..<pos | ||
annotations.append(LineAnnotation(style: "highlight", range: range)) | ||
} | ||
for line in strikeout { | ||
let pos = Position(line: line, character: nil) | ||
let range = pos..<pos | ||
annotations.append(LineAnnotation(style: "strikeout", range: range)) | ||
} | ||
self.lineAnnotations = annotations | ||
} | ||
|
||
public init(copyToClipboard: Bool, showLineNumbers: Bool, wrap: Int, lineAnnotations: [LineAnnotation]) { | ||
self.copyToClipboard = copyToClipboard | ||
self.showLineNumbers = showLineNumbers | ||
self.wrap = wrap | ||
self.lineAnnotations = lineAnnotations | ||
} | ||
|
||
/// A function that parses array values on code block options from the language line string | ||
static internal func parseCodeBlockOptionsArray(_ value: String?) -> [Int] { | ||
guard var s = value?.trimmingCharacters(in: .whitespaces), !s.isEmpty else { return [] } | ||
|
||
if s.hasPrefix("[") && s.hasSuffix("]") { | ||
s.removeFirst() | ||
s.removeLast() | ||
} | ||
|
||
return s.split(separator: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) } | ||
} | ||
|
||
/// A function that parses the language line options on code blocks, returning the language and tokens, an array of OptionName and option values | ||
static internal func tokenizeLanguageString(_ input: String?) -> (lang: String?, tokens: [(name: OptionName, value: String?)]) { | ||
guard let input else { return (lang: nil, tokens: []) } | ||
|
||
let parts = parseLanguageString(input) | ||
var tokens: [(OptionName, String?)] = [] | ||
var lang: String? = nil | ||
|
||
for (index, part) in parts.enumerated() { | ||
if let eq = part.firstIndex(of: "=") { | ||
let key = part[..<eq].trimmingCharacters(in: .whitespaces).lowercased() | ||
let value = part[part.index(after: eq)...].trimmingCharacters(in: .whitespaces) | ||
if key == "wrap" { | ||
tokens.append((.wrap, value)) | ||
} else if key == "highlight" { | ||
tokens.append((.highlight, value)) | ||
} else if key == "strikeout" { | ||
tokens.append((.strikeout, value)) | ||
} else { | ||
tokens.append((.unknown, key)) | ||
} | ||
} else { | ||
let key = part.trimmingCharacters(in: .whitespaces).lowercased() | ||
if key == "nocopy" { | ||
tokens.append((.nocopy, nil as String?)) | ||
} else if key == "showlinenumbers" { | ||
tokens.append((.showLineNumbers, nil as String?)) | ||
} else if key == "wrap" { | ||
tokens.append((.wrap, nil as String?)) | ||
} else if key == "highlight" { | ||
tokens.append((.highlight, nil as String?)) | ||
} else if key == "strikeout" { | ||
tokens.append((.strikeout, nil as String?)) | ||
} else if index == 0 && !key.contains("[") && !key.contains("]") { | ||
lang = key | ||
} else { | ||
tokens.append((.unknown, key)) | ||
} | ||
} | ||
} | ||
return (lang, tokens) | ||
} | ||
|
||
// helper function for tokenizeLanguageString to parse the language line | ||
static func parseLanguageString(_ input: String?) -> [Substring] { | ||
|
||
guard let input else { return [] } | ||
var parts: [Substring] = [] | ||
var start = input.startIndex | ||
var i = input.startIndex | ||
|
||
var bracketDepth = 0 | ||
|
||
while i < input.endIndex { | ||
let c = input[i] | ||
|
||
if c == "[" { bracketDepth += 1 } | ||
else if c == "]" { bracketDepth = max(0, bracketDepth - 1) } | ||
else if c == "," && bracketDepth == 0 { | ||
let seq = input[start..<i] | ||
if !seq.isEmpty { | ||
parts.append(seq) | ||
} | ||
input.formIndex(after: &i) | ||
start = i | ||
continue | ||
} | ||
input.formIndex(after: &i) | ||
} | ||
let tail = input[start..<input.endIndex] | ||
if !tail.isEmpty { | ||
parts.append(tail) | ||
} | ||
Comment on lines
+328
to
+347
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FIY: the part that increments the index and reads the characters can be simplified by iterating over the input zipped with its indices—we have an for (i, c) in input.indexed() {
switch c {
case "[": bracketDepth += 1
case "]": bracketDepth -= 1
case "," where bracketDepth == 0:
if start < i {
parts.append(input[start..<i])
}
start = input.index(after: i) // Don't include the "," in the next token
default: continue
}
}
if start < input.endIndex {
parts.append(input[start...])
} |
||
|
||
return parts | ||
} | ||
} | ||
|
||
|
@@ -711,7 +914,7 @@ extension RenderBlockContent.Table: Codable { | |
extension RenderBlockContent: Codable { | ||
private enum CodingKeys: CodingKey { | ||
case type | ||
case inlineContent, content, caption, style, name, syntax, code, level, text, items, media, runtimePreview, anchor, summary, example, metadata, start, copyToClipboard | ||
case inlineContent, content, caption, style, name, syntax, code, level, text, items, media, runtimePreview, anchor, summary, example, metadata, start, copyToClipboard, showLineNumbers, wrap, lineAnnotations | ||
case request, response | ||
case header, rows | ||
case numberOfColumns, columns | ||
|
@@ -734,12 +937,23 @@ extension RenderBlockContent: Codable { | |
self = try .aside(.init(style: style, content: container.decode([RenderBlockContent].self, forKey: .content))) | ||
case .codeListing: | ||
let copy = FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled | ||
let options: CodeBlockOptions? | ||
if !Set(container.allKeys).isDisjoint(with: [.copyToClipboard, .showLineNumbers, .wrap, .lineAnnotations]) { | ||
options = try CodeBlockOptions( | ||
copyToClipboard: container.decodeIfPresent(Bool.self, forKey: .copyToClipboard) ?? copy, | ||
showLineNumbers: container.decodeIfPresent(Bool.self, forKey: .showLineNumbers) ?? false, | ||
wrap: container.decodeIfPresent(Int.self, forKey: .wrap) ?? 0, | ||
lineAnnotations: container.decodeIfPresent([CodeBlockOptions.LineAnnotation].self, forKey: .lineAnnotations) ?? [] | ||
) | ||
} else { | ||
options = nil | ||
} | ||
self = try .codeListing(.init( | ||
syntax: container.decodeIfPresent(String.self, forKey: .syntax), | ||
code: container.decode([String].self, forKey: .code), | ||
metadata: container.decodeIfPresent(RenderContentMetadata.self, forKey: .metadata), | ||
copyToClipboard: container.decodeIfPresent(Bool.self, forKey: .copyToClipboard) ?? copy | ||
)) | ||
options: options | ||
)) | ||
case .heading: | ||
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))) | ||
case .orderedList: | ||
|
@@ -842,7 +1056,10 @@ extension RenderBlockContent: Codable { | |
try container.encode(l.syntax, forKey: .syntax) | ||
try container.encode(l.code, forKey: .code) | ||
try container.encodeIfPresent(l.metadata, forKey: .metadata) | ||
try container.encode(l.copyToClipboard, forKey: .copyToClipboard) | ||
try container.encodeIfPresent(l.options?.copyToClipboard, forKey: .copyToClipboard) | ||
try container.encodeIfPresent(l.options?.showLineNumbers, forKey: .showLineNumbers) | ||
try container.encodeIfPresent(l.options?.wrap, forKey: .wrap) | ||
try container.encodeIfPresent(l.options?.lineAnnotations, forKey: .lineAnnotations) | ||
case .heading(let h): | ||
try container.encode(h.level, forKey: .level) | ||
try container.encode(h.text, forKey: .text) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: this expression can be simplified by using
count(where:)
in combination with passing a key path for the closure