Files
sourcekit-lsp/Sources/SwiftLanguageService/CommentXML.swift
2025-10-31 14:11:11 -07:00

178 lines
4.6 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Foundation
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
#if canImport(FoundationXML)
import FoundationXML
#endif
enum CommentXMLError: Error {
case noRootElement
}
/// Converts from sourcekit's XML documentation format to Markdown.
///
/// This code should go away and sourcekitd should return the Markdown directly.
package func xmlDocumentationToMarkdown(_ xmlString: String) throws -> String {
let xml = try XMLDocument(xmlString: xmlString)
guard let root = xml.rootElement() else {
throw CommentXMLError.noRootElement
}
var convert = XMLToMarkdown()
convert.out.reserveCapacity(xmlString.utf16.count)
convert.toMarkdown(root)
return convert.out
}
private struct XMLToMarkdown {
var out: String = ""
var indentCount: Int = 0
let indentWidth: Int = 4
var lineNumber: Int = 0
var inParam: Bool = false
mutating func newlineIfNeeded(count: Int = 1) {
if !out.isEmpty && out.last! != "\n" {
newline(count: count)
}
}
mutating func newline(count: Int = 1) {
out += String(repeating: "\n", count: count)
out += String(repeating: " ", count: indentWidth * indentCount)
}
mutating func toMarkdown(_ node: XMLNode) {
switch node.kind {
case .element:
toMarkdown(node as! XMLElement)
default:
out += node.stringValue ?? ""
}
}
// [XMLNode]? is the type of XMLNode.children.
mutating func toMarkdown(_ nodes: [XMLNode]?, separator: String = "") {
guard let nodes else {
return
}
for node in nodes {
toMarkdown(node)
out += separator
}
}
mutating func toMarkdown(_ node: XMLElement) {
switch node.name {
case "Declaration":
newlineIfNeeded(count: 2)
out += "```swift\n"
toMarkdown(node.children)
out += "\n```\n"
case "Name", "USR", "Direction":
break
case "Abstract", "Para":
if !inParam {
newlineIfNeeded(count: 2)
}
toMarkdown(node.children)
case "Discussion", "ResultDiscussion", "ThrowsDiscussion":
if !inParam {
newlineIfNeeded(count: 2)
}
out += "### "
switch node.name {
case "Discussion": out += "Discussion"
case "ResultDiscussion": out += "Returns"
case "ThrowsDiscussion": out += "Throws"
default: fatalError("handled in outer switch")
}
newline(count: 2)
toMarkdown(node.children)
case "Parameters":
newlineIfNeeded(count: 2)
out += "- Parameters:"
indentCount += 1
toMarkdown(node.children)
indentCount -= 1
case "Parameter":
guard let name = node.elements(forName: "Name").first else { break }
newlineIfNeeded()
out += "- "
toMarkdown(name.children)
if let discussion = node.elements(forName: "Discussion").first {
out += ": "
inParam = true
toMarkdown(discussion.children)
inParam = false
}
case "CodeListing":
lineNumber = 0
newlineIfNeeded(count: 2)
out += "```\(node.attributes?.first(where: { $0.name == "language" })?.stringValue ?? "")\n"
toMarkdown(node.children, separator: "\n")
out += "```"
newlineIfNeeded(count: 2)
case "zCodeLineNumbered":
lineNumber += 1
out += "\(lineNumber).\t"
toMarkdown(node.children)
case "codeVoice":
out += "`"
toMarkdown(node.children)
out += "`"
case "emphasis":
out += "*"
toMarkdown(node.children)
out += "*"
case "bold":
out += "**"
toMarkdown(node.children)
out += "**"
case "h1", "h2", "h3", "h4", "h5", "h6":
newlineIfNeeded(count: 2)
let n = Int(node.name!.dropFirst())
out += String(repeating: "#", count: n!)
out += " "
toMarkdown(node.children)
out += "\n\n"
case "Link":
if let href = node.attributes?.first(where: { $0.name == "href" })?.stringValue {
out += "["
toMarkdown(node.children)
out += "](\(href))"
} else {
// Not a valid link.
toMarkdown(node.children)
}
default:
toMarkdown(node.children)
}
}
}