RFC: Additional Options for Code Blocks (highlight, strikeout, wrap, line numbers)

I'll also say that I think it's worth talking about the syntax for these types of richer annotations because it can draw attention to some syntax limitations.

For example, using the invalidFlip code block with error messages (from the "Opaque and Boxed Protocol Types" documentation) as an example to look at how various syntax could represent those error annotations:

func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape // Error: return types don't match
    }
    return FlippedShape(shape: shape) // Error: return types don't match
}

Here, each error annotation groups together 3 pieces of information:

  • a line and character range
  • a style ("error")
  • a message

The hypothetical syntax I used above groups this information like below:

```swift, showLineNumbers, highlighted
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape 
               ~~~~~ error: Return types don't match
    }
    return FlippedShape(shape: shape)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~ error: Return types don't match
}
```

The other two syntaxes each have different challenges with grouping information like this and with including longer form text as annotation information.

Assuming that the "fenced" line syntax uses a new list for each style, the first two could be grouped together like error=[3:15-20, 5:12-38]. It's possible that the error message could be interspersed with the ranges, representing a 3 pieces of the the first annotation like error=[3:15-20 "Return types don't match"]. That's not too bad on its own, but considering that all annotations have to fit on the same line, this can start to become cumbersome after only a couple of annotations (since potentially long messages are inlined):

```swift, showLineNumbers, error=[3:15-20 "Return types don't match", 5:12-38 "Return types don't match"]
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape
    }
    return FlippedShape(shape: shape)
}
```

Directive parameters have some of the same problems but has the benefit that each parameter can be specified on its own line. However, the parameters have to be primitives, meaning that more values may need to be represented as strings:

@CodeBlock(
  showLineNumbers: true,
  errorAnnotations: ["3:15-20 \"Return types don't match\"", "5:12-38 \"Return types don't match\""]
) {
  ```swift
  func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
      if shape is Square {
          return shape
      }
      return FlippedShape(shape: shape)
  }
  ```
}

Alternatively, directives can use nested directives to provide more complicated configuration:

@CodeBlock(showLineNumbers: true) {
  @Highlight(line: 3, start: 15, end: 20, style: "error") {
    Return types don't match
  }

  @Highlight(line: 5, start: 12, end: 38, style: "error") {
    Return types don't match
  }

  ```swift
  func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
      if shape is Square {
          return shape
      }
      return FlippedShape(shape: shape)
  }
  ```
}

This can be very flexible but it can also be fairly verbose and significantly impacts other tool's ability to process and display the code block.

2 Likes