diff --git a/README.md b/README.md index 4927a7ad..e9e9b6de 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Swift `Markdown` is a Swift package for parsing, building, editing, and analyzin The parser is powered by GitHub-flavored Markdown's [cmark-gfm](https://github.com/github/cmark-gfm) implementation, so it follows the spec closely. As the needs of the community change, the effective dialect implemented by this library may change. -The markup tree provided by this package is comprised of immutable/persistent, thread-safe, copy-on-write value types that only copy substructure that has changed. Other examples of the main strategy behind this library can be seen in Swift's [lib/Syntax](https://github.com/apple/swift/tree/master/lib/Syntax) and its Swift bindings, [SwiftSyntax](https://github.com/apple/swift-syntax). +The markup tree provided by this package is comprised of immutable/persistent, thread-safe, copy-on-write value types that only copy substructure that has changed. Other examples of the main strategy behind this library can be seen in [SwiftSyntax](https://github.com/apple/swift-syntax). ## Getting Started Using Markup diff --git a/Snippets/Parsing/test.md b/Snippets/Parsing/test.md index 7fab4475..6190ff3b 100644 --- a/Snippets/Parsing/test.md +++ b/Snippets/Parsing/test.md @@ -1,3 +1,5 @@ # Sample document This is a *sample document*. + + diff --git a/Sources/Markdown/Base/Markup.swift b/Sources/Markdown/Base/Markup.swift index bf51fe51..23c72e78 100644 --- a/Sources/Markdown/Base/Markup.swift +++ b/Sources/Markdown/Base/Markup.swift @@ -71,6 +71,10 @@ func makeMarkup(_ data: _MarkupData) -> Markup { return SymbolLink(data) case .inlineAttributes: return InlineAttributes(data) + case .doxygenParam: + return DoxygenParameter(data) + case .doxygenReturns: + return DoxygenReturns(data) } } @@ -261,7 +265,7 @@ extension Markup { public func child(through path: TypedChildIndexPath) -> Markup? { var element: Markup = self for pathElement in path { - guard pathElement.index <= raw.markup.childCount else { + guard pathElement.index <= element.childCount else { return nil } diff --git a/Sources/Markdown/Base/RawMarkup.swift b/Sources/Markdown/Base/RawMarkup.swift index 61d12760..3391265e 100644 --- a/Sources/Markdown/Base/RawMarkup.swift +++ b/Sources/Markdown/Base/RawMarkup.swift @@ -51,6 +51,9 @@ enum RawMarkupData: Equatable { case tableBody case tableRow case tableCell(colspan: UInt, rowspan: UInt) + + case doxygenParam(name: String) + case doxygenReturns } extension RawMarkupData { @@ -330,6 +333,14 @@ final class RawMarkup: ManagedBuffer { static func tableCell(parsedRange: SourceRange?, colspan: UInt, rowspan: UInt, _ children: [RawMarkup]) -> RawMarkup { return .create(data: .tableCell(colspan: colspan, rowspan: rowspan), parsedRange: parsedRange, children: children) } + + static func doxygenParam(name: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { + return .create(data: .doxygenParam(name: name), parsedRange: parsedRange, children: children) + } + + static func doxygenReturns(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { + return .create(data: .doxygenReturns, parsedRange: parsedRange, children: children) + } } fileprivate extension Sequence where Element == RawMarkup { diff --git a/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenParameter.swift b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenParameter.swift new file mode 100644 index 00000000..3f4df5a7 --- /dev/null +++ b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenParameter.swift @@ -0,0 +1,78 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2023 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 Swift project authors +*/ + +import Foundation + +/// A parsed Doxygen `\param` command. +/// +/// The Doxygen support in Swift-Markdown parses `\param` commands of the form +/// `\param name description`, where `description` extends until the next blank line or the next +/// parsed command. For example, the following input will return two `DoxygenParam` instances: +/// +/// ```markdown +/// \param coordinate The coordinate used to center the transformation. +/// \param matrix The transformation matrix that describes the transformation. +/// For more information about transformation matrices, refer to the Transformation +/// documentation. +/// ``` +public struct DoxygenParameter: BlockContainer { + public var _data: _MarkupData + + init(_ raw: RawMarkup) throws { + guard case .doxygenParam = raw.data else { + throw RawMarkup.Error.concreteConversionError(from: raw, to: DoxygenParameter.self) + } + let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0)) + self.init(_MarkupData(absoluteRaw)) + } + + init(_ data: _MarkupData) { + self._data = data + } + + public func accept(_ visitor: inout V) -> V.Result { + return visitor.visitDoxygenParameter(self) + } +} + +public extension DoxygenParameter { + /// Create a new Doxygen parameter definition. + /// + /// - Parameter name: The name of the parameter being described. + /// - Parameter children: Block child elements. + init(name: String, children: Children) where Children.Element == BlockMarkup { + try! self.init(.doxygenParam(name: name, parsedRange: nil, children.map({ $0.raw.markup }))) + } + + /// Create a new Doxygen parameter definition. + /// + /// - Parameter name: The name of the parameter being described. + /// - Parameter children: Block child elements. + init(name: String, children: BlockMarkup...) { + self.init(name: name, children: children) + } + + /// The name of the parameter being described. + var name: String { + get { + guard case let .doxygenParam(name) = _data.raw.markup.data else { + fatalError("\(self) markup wrapped unexpected \(_data.raw)") + } + return name + } + set { + _data = _data.replacingSelf(.doxygenParam( + name: newValue, + parsedRange: nil, + _data.raw.markup.copyChildren() + )) + } + } +} diff --git a/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift new file mode 100644 index 00000000..5a6cbd57 --- /dev/null +++ b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift @@ -0,0 +1,58 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2023 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 Swift project authors +*/ + +import Foundation + +/// A parsed Doxygen `\returns`, `\return`, or `\result` command. +/// +/// The Doxygen support in Swift-Markdown parses `\returns` commands of the form +/// `\returns description`, where `description` continues until the next blank line or parsed +/// command. The commands `\return` and `\result` are also accepted, with the same format. +/// +/// ```markdown +/// \returns A freshly-created object. +/// ``` +public struct DoxygenReturns: BlockContainer { + public var _data: _MarkupData + + init(_ raw: RawMarkup) throws { + guard case .doxygenReturns = raw.data else { + throw RawMarkup.Error.concreteConversionError(from: raw, to: DoxygenReturns.self) + } + let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0)) + self.init(_MarkupData(absoluteRaw)) + } + + init(_ data: _MarkupData) { + self._data = data + } + + public func accept(_ visitor: inout V) -> V.Result { + return visitor.visitDoxygenReturns(self) + } +} + +public extension DoxygenReturns { + /// Create a new Doxygen parameter definition. + /// + /// - Parameter name: The name of the parameter being described. + /// - Parameter children: Block child elements. + init(children: Children) where Children.Element == BlockMarkup { + try! self.init(.doxygenReturns(parsedRange: nil, children.map({ $0.raw.markup }))) + } + + /// Create a new Doxygen parameter definition. + /// + /// - Parameter name: The name of the parameter being described. + /// - Parameter children: Block child elements. + init(children: BlockMarkup...) { + self.init(children: children) + } +} diff --git a/Sources/Markdown/Inline Nodes/Inline Containers/Link.swift b/Sources/Markdown/Inline Nodes/Inline Containers/Link.swift index 54ae3d8f..1dc70db1 100644 --- a/Sources/Markdown/Inline Nodes/Inline Containers/Link.swift +++ b/Sources/Markdown/Inline Nodes/Inline Containers/Link.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2023 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 @@ -63,6 +63,16 @@ public extension Link { } } + var isAutolink: Bool { + guard let destination = destination, + childCount == 1, + let text = child(at: 0) as? Text, + destination == text.string else { + return false + } + return true + } + // MARK: Visitation func accept(_ visitor: inout V) -> V.Result { diff --git a/Sources/Markdown/Markdown.docc/Markdown.md b/Sources/Markdown/Markdown.docc/Markdown.md index 0821a72c..89a664cb 100644 --- a/Sources/Markdown/Markdown.docc/Markdown.md +++ b/Sources/Markdown/Markdown.docc/Markdown.md @@ -6,7 +6,7 @@ Swift `Markdown` is a Swift package for parsing, building, editing, and analyzin The parser is powered by GitHub-flavored Markdown's [cmark-gfm](https://github.com/github/cmark-gfm) implementation, so it follows the spec closely. As the needs of the community change, the effective dialect implemented by this library may change. -The markup tree provided by this package is comprised of immutable/persistent, thread-safe, copy-on-write value types that only copy substructure that has changed. Other examples of the main strategy behind this library can be seen in Swift's [lib/Syntax](https://github.com/apple/swift/tree/master/lib/Syntax) and its Swift bindings, [SwiftSyntax](https://github.com/apple/swift-syntax). +The markup tree provided by this package is comprised of immutable/persistent, thread-safe, copy-on-write value types that only copy substructure that has changed. Other examples of the main strategy behind this library can be seen in [SwiftSyntax](https://github.com/apple/swift-syntax). ## Topics diff --git a/Sources/Markdown/Markdown.docc/Markdown/BlockMarkup.md b/Sources/Markdown/Markdown.docc/Markdown/BlockMarkup.md index f3eeb711..048ff444 100644 --- a/Sources/Markdown/Markdown.docc/Markdown/BlockMarkup.md +++ b/Sources/Markdown/Markdown.docc/Markdown/BlockMarkup.md @@ -26,5 +26,6 @@ ## See Also - +- diff --git a/Sources/Markdown/Markdown.docc/Markdown/DoxygenCommands.md b/Sources/Markdown/Markdown.docc/Markdown/DoxygenCommands.md new file mode 100644 index 00000000..ced3c42b --- /dev/null +++ b/Sources/Markdown/Markdown.docc/Markdown/DoxygenCommands.md @@ -0,0 +1,50 @@ +# Doxygen Commands + +Include a limited set of Doxygen commands in parsed Markdown. + +Swift Markdown includes an option to parse a limited set of Doxygen commands, to facilitate +transitioning from a different Markdown parser. To include these commands in the output, include +the options ``ParseOptions/parseBlockDirectives`` and ``ParseOptions/parseMinimalDoxygen`` when +parsing a ``Document``. In the resulting document, parsed Doxygen commands appear as regular +``Markup`` types in the hierarchy. + +## Parsing Strategy + +Doxygen commands are written by using either a backslash (`\`) or an at-sign (`@`) character, +followed by the name of the command. Any parameters are then parsed as whitespace-separated words, +then a "description" argument is taken from the remainder of the line, as well as all lines +immediately after the command, until the parser sees a blank line, another Doxygen command, or a +block directive. The description is then parsed for regular Markdown formatting, which is then +stored as the children of the command type. For example, with Doxygen parsing turned on, the +following document will parse three separate commands and one block directive: + +```markdown +\param thing The thing. +This is the thing that is modified. +\param otherThing The other thing. + +\returns A thing that has been modified. +@Comment { + This is not part of the `\returns` command. +} +``` + +Trailing lines in a command's description are allowed to be indented relative to the command. For +example, the description below is parsed as a paragraph, not a code block: + +```markdown +\param thing + The thing. + This is the thing that is modified. +``` + +Doxygen commands are not parsed within code blocks or block directive content. + +## Topics + +### Commands + +- ``DoxygenParam`` +- ``DoxygenReturns`` + + diff --git a/Sources/Markdown/Markdown.docc/Snippets.md b/Sources/Markdown/Markdown.docc/Snippets.md index e0c646e0..e8b0b741 100644 --- a/Sources/Markdown/Markdown.docc/Snippets.md +++ b/Sources/Markdown/Markdown.docc/Snippets.md @@ -41,3 +41,5 @@ a Markdown document to a consistent, preferred style. @Snippet(path: "swift-markdown/Snippets/Formatting/PreferredHeadingStyle") @Snippet(path: "swift-markdown/Snippets/Formatting/ThematicBreakCharacter") @Snippet(path: "swift-markdown/Snippets/Formatting/UseCodeFence") + + diff --git a/Sources/Markdown/Parser/BlockDirectiveParser.swift b/Sources/Markdown/Parser/BlockDirectiveParser.swift index 8792c96c..17d7f04c 100644 --- a/Sources/Markdown/Parser/BlockDirectiveParser.swift +++ b/Sources/Markdown/Parser/BlockDirectiveParser.swift @@ -182,6 +182,7 @@ struct PendingBlockDirective { // "@xx { yy } zz }" "yy } zz" will be parsed var reversedRemainingContent = TrimmedLine(Substring(line.text.reversed()), source: line.source, lineNumber: line.lineNumber) + reversedRemainingContent.lexWhitespace() if !line.text.isEmpty, reversedRemainingContent.lex("}") != nil { let trailingWhiteSpaceCount = reversedRemainingContent.lexWhitespace()?.text.count ?? 0 @@ -189,9 +190,7 @@ struct PendingBlockDirective { let leadingSpacingCount = line.untrimmedText.count - textCount - trailingWhiteSpaceCount - 1 innerIndentationColumnCount = leadingSpacingCount // Should we add a new property for this kind of usage? - let startIndex = line.untrimmedText.startIndex - let endIndex = line.untrimmedText.index(startIndex, offsetBy: leadingSpacingCount) - let newLine = line.untrimmedText.replacingCharacters(in: startIndex..(parsingHierarchyFrom trimmedLines: TrimmedLines) where TrimmedLines.Element == TrimmedLine { - self = ParseContainerStack(parsingHierarchyFrom: trimmedLines).top + /// A Doxygen command, which can contain arbitrary markup (but not block directives). + case doxygenCommand(PendingDoxygenCommand, [TrimmedLine]) + + init(parsingHierarchyFrom trimmedLines: TrimmedLines, options: ParseOptions) where TrimmedLines.Element == TrimmedLine { + self = ParseContainerStack(parsingHierarchyFrom: trimmedLines, options: options).top } var children: [ParseContainer] { @@ -471,6 +503,8 @@ private enum ParseContainer: CustomStringConvertible { return children case .lineRun: return [] + case .doxygenCommand: + return [] } } @@ -545,6 +579,15 @@ private enum ParseContainer: CustomStringConvertible { indent -= 4 } print(children: children) + case .doxygenCommand(let pendingDoxygenCommand, let lines): + print("* Doxygen command \(pendingDoxygenCommand.kind.debugDescription)") + queueNewline() + indent += 4 + for line in lines { + print(line.text.debugDescription) + queueNewline() + } + indent -= 4 } } } @@ -565,6 +608,9 @@ private enum ParseContainer: CustomStringConvertible { case .blockDirective(var pendingBlockDirective, let children): pendingBlockDirective.updateIndentation(for: line) self = .blockDirective(pendingBlockDirective, children) + case .doxygenCommand: + var newParent: ParseContainer? = nil + parent?.updateIndentation(under: &newParent, for: line) } } @@ -609,6 +655,8 @@ private enum ParseContainer: CustomStringConvertible { return parent?.indentationAdjustment(under: nil) ?? 0 case .blockDirective(let pendingBlockDirective, _): return pendingBlockDirective.indentationColumnCount + case .doxygenCommand(let pendingCommand, _): + return pendingCommand.indentationAdjustment } } @@ -631,10 +679,11 @@ private enum ParseContainer: CustomStringConvertible { // We need to keep track of what we removed because cmark will report different source locations than what we // had in the source. We'll adjust those when we get them back. let trimmedIndentationAndLines = lines.map { line -> (line: TrimmedLine, - indentation: TrimmedLine.Lex?) in + indentation: Int) in var trimmedLine = line let trimmedWhitespace = trimmedLine.lexWhitespace(maxLength: indentationColumnCount) - return (trimmedLine, trimmedWhitespace) + let indentation = (trimmedWhitespace?.text.count ?? 0) + line.untrimmedText.distance(from: line.untrimmedText.startIndex, to: line.parseIndex) + return (trimmedLine, indentation) } // Build the logical block of text that cmark will see. @@ -677,6 +726,17 @@ private enum ParseContainer: CustomStringConvertible { }), parsedRange: pendingBlockDirective.atLocation..(parsingHierarchyFrom trimmedLines: TrimmedLines) where TrimmedLines.Element == TrimmedLine { + private let options: ParseOptions + + init(parsingHierarchyFrom trimmedLines: TrimmedLines, options: ParseOptions) where TrimmedLines.Element == TrimmedLine { self.stack = [.root([])] + self.options = options for line in trimmedLines { accept(line) } @@ -708,6 +771,20 @@ struct ParseContainerStack { } != nil } + private var canParseDoxygenCommand: Bool { + guard options.contains(.parseMinimalDoxygen) else { return false } + + guard !isInBlockDirective else { return false } + + if case .blockDirective = top { + return false + } else if case .lineRun(_, isInCodeFence: let codeFence) = top { + return !codeFence + } else { + return true + } + } + private func isCodeFenceOrIndentedCodeBlock(on line: TrimmedLine) -> Bool { // Check if this line is indented 4 or more spaces relative to the current // indentation adjustment. @@ -760,23 +837,96 @@ struct ParseContainerStack { return pendingBlockDirective } + private func parseDoxygenCommandOpening(on line: TrimmedLine) -> (pendingCommand: PendingDoxygenCommand, remainder: TrimmedLine)? { + guard canParseDoxygenCommand else { return nil } + guard !isCodeFenceOrIndentedCodeBlock(on: line) else { return nil } + + var remainder = line + let indent = remainder.lexWhitespace() + guard let at = remainder.lex(until: { ch in + switch ch { + case "@", "\\": + return .continue + default: + return .stop + } + }) else { return nil } + guard let name = remainder.lex(until: { ch in + if ch.isWhitespace { + return .stop + } else { + return .continue + } + }) else { return nil } + remainder.lexWhitespace() + + switch name.text.lowercased() { + case "param": + guard let paramName = remainder.lex(until: { ch in + if ch.isWhitespace { + return .stop + } else { + return .continue + } + }) else { return nil } + remainder.lexWhitespace() + var pendingCommand = PendingDoxygenCommand( + atLocation: at.range!.lowerBound, + atSignIndentation: indent?.text.count ?? 0, + nameLocation: name.range!.lowerBound, + kind: .param(name: paramName.text), + endLocation: name.range!.upperBound) + pendingCommand.addLine(remainder) + return (pendingCommand, remainder) + case "return", "returns", "result": + var pendingCommand = PendingDoxygenCommand( + atLocation: at.range!.lowerBound, + atSignIndentation: indent?.text.count ?? 0, + nameLocation: name.range!.lowerBound, + kind: .returns, + endLocation: name.range!.upperBound) + pendingCommand.addLine(remainder) + return (pendingCommand, remainder) + default: + return nil + } + } + /// Accept a trimmed line, opening new block directives as indicated by the source, /// closing a block directive if applicable, or adding the line to a run of lines to be parsed /// as Markdown later. private mutating func accept(_ line: TrimmedLine) { - if line.isEmptyOrAllWhitespace, - case let .blockDirective(pendingBlockDirective, _) = top { - switch pendingBlockDirective.parseState { - case .argumentsStart, - .contentsStart, - .done: - closeTop() + if line.isEmptyOrAllWhitespace { + switch top { + case let .blockDirective(pendingBlockDirective, _): + switch pendingBlockDirective.parseState { + case .argumentsStart, + .contentsStart, + .done: + closeTop() + default: + break + } + case .doxygenCommand: + closeTop() default: break } } + // If we can parse a Doxygen command from this line, start one and skip everything else. + if let result = parseDoxygenCommandOpening(on: line) { + switch top { + case .root: + break + default: + closeTop() + } + push(.doxygenCommand(result.pendingCommand, [result.remainder])) + return + } + // If we're inside a block directive, check to see whether we need to update its // indentation calculation to account for its content. updateIndentation(for: line) @@ -822,7 +972,7 @@ struct ParseContainerStack { switch top { case .root: push(.blockDirective(newBlockDirective, [])) - case .lineRun: + case .lineRun, .doxygenCommand: closeTop() push(.blockDirective(newBlockDirective, [])) case .blockDirective(let previousBlockDirective, _): @@ -848,16 +998,24 @@ struct ParseContainerStack { } else { switch top { case .root: - push(.lineRun([line], isInCodeFence: false)) + push(.lineRun([line], isInCodeFence: line.isProbablyCodeFence)) case .lineRun(var lines, let isInCodeFence): pop() lines.append(line) push(.lineRun(lines, isInCodeFence: isInCodeFence != line.isProbablyCodeFence)) + case .doxygenCommand(var pendingDoxygenCommand, var lines): + pop() + lines.append(line) + pendingDoxygenCommand.addLine(line) + push(.doxygenCommand(pendingDoxygenCommand, lines)) case .blockDirective(var pendingBlockDirective, let children): // A pending block directive can accept this line if it is in the middle of // parsing arguments text (to allow indentation to align arguments) or // if the line isn't taking part in a code block. - let canAcceptLine = pendingBlockDirective.parseState == .argumentsText || !isCodeFenceOrIndentedCodeBlock(on: line) + let canAcceptLine = + pendingBlockDirective.parseState != .done && + (pendingBlockDirective.parseState == .argumentsText || + !isCodeFenceOrIndentedCodeBlock(on: line)) if canAcceptLine && pendingBlockDirective.accept(line) { pop() push(.blockDirective(pendingBlockDirective, children)) @@ -923,6 +1081,8 @@ struct ParseContainerStack { push(.blockDirective(pendingBlockDirective, children)) case .lineRun: fatalError("Line runs cannot have children") + case .doxygenCommand: + fatalError("Doxygen commands cannot have children") } } @@ -985,7 +1145,7 @@ struct BlockDirectiveParser { // Phase 1: Categorize the lines into a hierarchy of block containers by parsing the prefix // of the line, opening and closing block directives appropriately, and folding elements // into a root document. - let rootContainer = ParseContainer(parsingHierarchyFrom: trimmedLines) + let rootContainer = ParseContainer(parsingHierarchyFrom: trimmedLines, options: options) // Phase 2: Convert the hierarchy of block containers into a real ``Document``. // This is where the CommonMark parser is called upon to parse runs of lines of content, diff --git a/Sources/Markdown/Parser/ParseOptions.swift b/Sources/Markdown/Parser/ParseOptions.swift index 62356547..817e9b46 100644 --- a/Sources/Markdown/Parser/ParseOptions.swift +++ b/Sources/Markdown/Parser/ParseOptions.swift @@ -24,5 +24,8 @@ public struct ParseOptions: OptionSet { /// Disable converting straight quotes to curly, --- to em dashes, -- to en dashes during parsing public static let disableSmartOpts = ParseOptions(rawValue: 1 << 2) + + /// Parse a limited set of Doxygen commands. Requires ``parseBlockDirectives``. + public static let parseMinimalDoxygen = ParseOptions(rawValue: 1 << 3) } diff --git a/Sources/Markdown/Parser/RangeAdjuster.swift b/Sources/Markdown/Parser/RangeAdjuster.swift index 6fbb1420..88ab0e58 100644 --- a/Sources/Markdown/Parser/RangeAdjuster.swift +++ b/Sources/Markdown/Parser/RangeAdjuster.swift @@ -19,7 +19,7 @@ struct RangeAdjuster: MarkupWalker { /// An array of whitespace spans that were removed for each line, indexed /// by line number. `nil` means that no whitespace was removed on that line. - var trimmedIndentationPerLine: [TrimmedLine.Lex?] + var trimmedIndentationPerLine: [Int] mutating func defaultVisit(_ markup: Markup) { /// This should only be used in the parser where ranges are guaranteed @@ -27,10 +27,10 @@ struct RangeAdjuster: MarkupWalker { let adjustedRange = markup.range.map { range -> SourceRange in // Add back the offset to the column as if the indentation weren't stripped. let start = SourceLocation(line: startLine + range.lowerBound.line - 1, - column: range.lowerBound.column + (trimmedIndentationPerLine[range.lowerBound.line - 1]?.text.count ?? 0), + column: range.lowerBound.column + (trimmedIndentationPerLine[range.lowerBound.line - 1] ), source: range.lowerBound.source) let end = SourceLocation(line: startLine + range.upperBound.line - 1, - column: range.upperBound.column + (trimmedIndentationPerLine[range.upperBound.line - 1]?.text.count ?? 0), + column: range.upperBound.column + (trimmedIndentationPerLine[range.upperBound.line - 1]), source: range.upperBound.source) return start.. Result + + /** + Visit a `DoxygenParam` element and return the result. + + - parameter doxygenParam: A `DoxygenParam` element. + - returns: The result of the visit. + */ + mutating func visitDoxygenParameter(_ doxygenParam: DoxygenParameter) -> Result + + /** + Visit a `DoxygenReturns` element and return the result. + + - parameter doxygenReturns: A `DoxygenReturns` element. + - returns: The result of the visit. + */ + mutating func visitDoxygenReturns(_ doxygenReturns: DoxygenReturns) -> Result } extension MarkupVisitor { @@ -373,4 +389,10 @@ extension MarkupVisitor { public mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result { return defaultVisit(attributes) } + public mutating func visitDoxygenParameter(_ doxygenParam: DoxygenParameter) -> Result { + return defaultVisit(doxygenParam) + } + public mutating func visitDoxygenReturns(_ doxygenReturns: DoxygenReturns) -> Result { + return defaultVisit(doxygenReturns) + } } diff --git a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift index ddeb5535..ca74ea36 100644 --- a/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift +++ b/Sources/Markdown/Walker/Walkers/MarkupFormatter.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2023 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 @@ -230,6 +230,15 @@ public struct MarkupFormatter: MarkupWalker { } } + /// The character to use when formatting Doxygen commands. + public enum DoxygenCommandPrefix: String, CaseIterable { + /// Precede Doxygen commands with a backslash (`\`). + case backslash = "\\" + + /// Precede Doxygen commands with an at-sign (`@`). + case at = "@" + } + // MARK: Option Properties var orderedListNumerals: OrderedListNumerals @@ -244,6 +253,7 @@ public struct MarkupFormatter: MarkupWalker { var preferredHeadingStyle: PreferredHeadingStyle var preferredLineLimit: PreferredLineLimit? var customLinePrefix: String + var doxygenCommandPrefix: DoxygenCommandPrefix /** Create a set of formatting options to use when printing an element. @@ -273,7 +283,8 @@ public struct MarkupFormatter: MarkupWalker { condenseAutolinks: Bool = true, preferredHeadingStyle: PreferredHeadingStyle = .atx, preferredLineLimit: PreferredLineLimit? = nil, - customLinePrefix: String = "") { + customLinePrefix: String = "", + doxygenCommandPrefix: DoxygenCommandPrefix = .backslash) { self.unorderedListMarker = unorderedListMarker self.orderedListNumerals = orderedListNumerals self.useCodeFence = useCodeFence @@ -288,6 +299,7 @@ public struct MarkupFormatter: MarkupWalker { // three characters long. self.thematicBreakLength = max(3, thematicBreakLength) self.customLinePrefix = customLinePrefix + self.doxygenCommandPrefix = doxygenCommandPrefix } /// The default set of formatting options. @@ -811,11 +823,8 @@ public struct MarkupFormatter: MarkupWalker { public mutating func visitLink(_ link: Link) { let savedState = state if formattingOptions.condenseAutolinks, - let destination = link.destination, - link.childCount == 1, - let text = link.child(at: 0) as? Text, - // Print autolink-style - destination == text.string { + link.isAutolink, + let destination = link.destination { print("<\(destination)>", for: link) } else { func printRegularLink() { @@ -1148,4 +1157,16 @@ public struct MarkupFormatter: MarkupWalker { printInlineAttributes() } } + + public mutating func visitDoxygenParameter(_ doxygenParam: DoxygenParameter) -> () { + print("\(formattingOptions.doxygenCommandPrefix.rawValue)param", for: doxygenParam) + print(" \(doxygenParam.name) ", for: doxygenParam) + descendInto(doxygenParam) + } + + public mutating func visitDoxygenReturns(_ doxygenReturns: DoxygenReturns) -> () { + // FIXME: store the actual command name used in the original markup + print("\(formattingOptions.doxygenCommandPrefix.rawValue)returns ", for: doxygenReturns) + descendInto(doxygenReturns) + } } diff --git a/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift b/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift index 8f5cc8aa..40594d18 100644 --- a/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift +++ b/Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift @@ -286,4 +286,8 @@ struct MarkupTreeDumper: MarkupWalker { mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> () { dump(attributes, customDescription: "attributes: `\(attributes.attributes)`") } + + mutating func visitDoxygenParameter(_ doxygenParam: DoxygenParameter) -> () { + dump(doxygenParam, customDescription: "parameter: \(doxygenParam.name)") + } } diff --git a/Sources/markdown-tool/Commands/DumpTreeCommand.swift b/Sources/markdown-tool/Commands/DumpTreeCommand.swift index 2b166103..ad2185e1 100644 --- a/Sources/markdown-tool/Commands/DumpTreeCommand.swift +++ b/Sources/markdown-tool/Commands/DumpTreeCommand.swift @@ -28,8 +28,18 @@ extension MarkdownCommand { @Flag(inversion: .prefixedNo, exclusivity: .chooseLast, help: "Parse block directives") var parseBlockDirectives: Bool = false + @Flag(inversion: .prefixedNo, exclusivity: .chooseLast, help: "Parse a minimal set of Doxygen commands (requires --parse-block-directives)") + var experimentalParseDoxygenCommands: Bool = false + func run() throws { - let parseOptions: ParseOptions = parseBlockDirectives ? [.parseBlockDirectives] : [] + var parseOptions = ParseOptions() + if parseBlockDirectives { + parseOptions.insert(.parseBlockDirectives) + } + if experimentalParseDoxygenCommands { + parseOptions.insert(.parseMinimalDoxygen) + } + let document: Document if let inputFilePath = inputFilePath { (_, document) = try MarkdownCommand.parseFile(at: inputFilePath, options: parseOptions) diff --git a/Tests/MarkdownTests/Base/MarkupTests.swift b/Tests/MarkdownTests/Base/MarkupTests.swift index 4258a921..15730437 100644 --- a/Tests/MarkdownTests/Base/MarkupTests.swift +++ b/Tests/MarkdownTests/Base/MarkupTests.swift @@ -283,6 +283,21 @@ final class MarkupTests: XCTestCase { )!.debugDescription() ) } + + func testChildThroughIndicesWithMultipleParagraphs() { + let source = """ +This is a markup __*document*__ with *some* **more** attributes. + +This is the *second* paragraph. +This is on a **new** line, but, continues on the same paragraph. + +This is the *third* paragraph. +This is on a **new** line, but, continues on the same paragraph. +""" + + let document = Document(parsing: source) + XCTAssertNotNil(document.child(through: [2, 5]) as? Strong) + } func testChildAtPositionHasCorrectType() throws { let source = "This is a [*link*](github.com). And some **bold** and *italic* text." diff --git a/Tests/MarkdownTests/Inline Nodes/LineBreakTests.swift b/Tests/MarkdownTests/Inline Nodes/LineBreakTests.swift index 369d27e5..c3081f23 100644 --- a/Tests/MarkdownTests/Inline Nodes/LineBreakTests.swift +++ b/Tests/MarkdownTests/Inline Nodes/LineBreakTests.swift @@ -25,4 +25,39 @@ final class LineBreakTests: XCTestCase { let paragraph = document.child(at: 0) as! Paragraph XCTAssertTrue(Array(paragraph.children)[1] is LineBreak) } + + /// Test that hard line breaks work with spaces (two or more). + func testSpaceHardLineBreak() { + let source = """ + Paragraph.\(" ") + Still the same paragraph. + """ + let document = Document(parsing: source) + let paragraph = document.child(at: 0) as! Paragraph + XCTAssertTrue(Array(paragraph.children)[1] is LineBreak) + } + + /// Test that hard line breaks work with a slash. + func testSlashHardLineBreak() { + let source = #""" + Paragraph.\ + Still the same paragraph. + """# + let document = Document(parsing: source) + let paragraph = document.child(at: 0) as! Paragraph + XCTAssertTrue(Array(paragraph.children)[1] is LineBreak) + } + + /// Sanity test that a multiline text without hard breaks doesn't return line breaks. + func testLineBreakWithout() { + let source = """ + Paragraph. + Same line text. + """ + let document = Document(parsing: source) + + let paragraph = document.child(at: 0) as! Paragraph + XCTAssertFalse(Array(paragraph.children)[1] is LineBreak) + XCTAssertEqual(Array(paragraph.children)[1].withoutSoftBreaks?.childCount, nil) + } } diff --git a/Tests/MarkdownTests/Inline Nodes/LinkTests.swift b/Tests/MarkdownTests/Inline Nodes/LinkTests.swift index 895b66af..aeeca9be 100644 --- a/Tests/MarkdownTests/Inline Nodes/LinkTests.swift +++ b/Tests/MarkdownTests/Inline Nodes/LinkTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2023 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 @@ -34,4 +34,18 @@ class LinkTests: XCTestCase { """ XCTAssertEqual(expectedDump, link.debugDescription()) } + + func testAutoLink() { + let children = [Text("example.com")] + var link = Link(destination: "example.com", children) + let expectedDump = """ + Link destination: "example.com" + └─ Text "example.com" + """ + XCTAssertEqual(expectedDump, link.debugDescription()) + XCTAssertTrue(link.isAutolink) + + link.destination = "test.example.com" + XCTAssertFalse(link.isAutolink) + } } diff --git a/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift b/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift index 6d4338bd..1849419d 100644 --- a/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift +++ b/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift @@ -982,4 +982,65 @@ class BlockDirectiveArgumentParserTests: XCTestCase { """# XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump) } + + func testSingleLineDirectiveWithTrailingWhitespace() { + let source = """ + @blah { content }\(" ") + @blah { + content + } + """ + let document = Document(parsing: source, options: [.parseBlockDirectives]) + + let expectedDump = #""" + Document @1:1-4:2 + ├─ BlockDirective @1:1-1:19 name: "blah" + │ └─ Paragraph @1:9-1:17 + │ └─ Text @1:9-1:16 "content" + └─ BlockDirective @2:1-4:2 name: "blah" + └─ Paragraph @3:5-3:12 + └─ Text @3:5-3:12 "content" + """# + XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump) + } + + func testSingleLineDirectiveWithTrailingContent() { + let source = """ + @blah { content } + content + """ + let document = Document(parsing: source, options: [.parseBlockDirectives]) + let expectedDump = #""" + Document @1:1-2:8 + ├─ BlockDirective @1:1-1:18 name: "blah" + │ └─ Paragraph @1:9-1:16 + │ └─ Text @1:9-1:16 "content" + └─ Paragraph @2:1-2:8 + └─ Text @2:1-2:8 "content" + """# + XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump) + } + + func testParsingTreeDumpFollowedByDirective() { + let source = """ + Document + ├─ Heading level: 1 + │ └─ Text "Title" + @Comment { Line c This is a single-line comment } + """ + let documentation = Document(parsing: source, options: .parseBlockDirectives) + let expected = """ + Document + ├─ Paragraph + │ ├─ Text "Document" + │ ├─ SoftBreak + │ ├─ Text "├─ Heading level: 1" + │ ├─ SoftBreak + │ └─ Text "│ └─ Text “Title”" + └─ BlockDirective name: "Comment" + └─ Paragraph + └─ Text "Line c This is a single-line comment" + """ + XCTAssertEqual(expected, documentation.debugDescription()) + } } diff --git a/Tests/MarkdownTests/Parsing/DoxygenCommandParserTests.swift b/Tests/MarkdownTests/Parsing/DoxygenCommandParserTests.swift new file mode 100644 index 00000000..7ce4f534 --- /dev/null +++ b/Tests/MarkdownTests/Parsing/DoxygenCommandParserTests.swift @@ -0,0 +1,403 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2023 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 Swift project authors +*/ + +@testable import Markdown +import XCTest + +class DoxygenCommandParserTests: XCTestCase { + let parseOptions: ParseOptions = [.parseMinimalDoxygen, .parseBlockDirectives] + + func testParseParam() throws { + let source = """ + @param thing The thing. + """ + + let document = Document(parsing: source, options: parseOptions) + let param = try XCTUnwrap(document.child(at: 0) as? DoxygenParameter) + XCTAssertEqual(param.name, "thing") + + let expectedDump = """ + Document + └─ DoxygenParameter parameter: thing + └─ Paragraph + └─ Text "The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testParseReturns() { + func assertValidParse(source: String) { + let document = Document(parsing: source, options: parseOptions) + XCTAssert(document.child(at: 0) is DoxygenReturns) + + let expectedDump = """ + Document + └─ DoxygenReturns + └─ Paragraph + └─ Text "The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + assertValidParse(source: "@returns The thing.") + assertValidParse(source: "@return The thing.") + assertValidParse(source: "@result The thing.") + assertValidParse(source: #"\returns The thing."#) + assertValidParse(source: #"\return The thing."#) + assertValidParse(source: #"\result The thing."#) + } + + func testParseParamWithSlash() throws { + let source = #""" + \param thing The thing. + """# + + let document = Document(parsing: source, options: parseOptions) + let param = try XCTUnwrap(document.child(at: 0) as? DoxygenParameter) + XCTAssertEqual(param.name, "thing") + + let expectedDump = """ + Document + └─ DoxygenParameter parameter: thing + └─ Paragraph + └─ Text "The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testParseMultilineDescription() { + let source = """ + @param thing The thing. + This is the thing that is messed with. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + └─ DoxygenParameter parameter: thing + └─ Paragraph + ├─ Text "The thing." + ├─ SoftBreak + └─ Text "This is the thing that is messed with." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testParseIndentedDescription() { + let source = """ + @param thing + The thing. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + └─ DoxygenParameter parameter: thing + └─ Paragraph + └─ Text "The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testParseMultilineIndentedDescription() { + let source = """ + @param thing The thing. + This is the thing that is messed with. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + └─ DoxygenParameter parameter: thing + └─ Paragraph + ├─ Text "The thing." + ├─ SoftBreak + └─ Text "This is the thing that is messed with." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testParseWithIndentedAtSign() { + let source = """ + Method description. + + @param thing The thing. + This is the thing that is messed with. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + ├─ Paragraph + │ └─ Text "Method description." + └─ DoxygenParameter parameter: thing + └─ Paragraph + ├─ Text "The thing." + ├─ SoftBreak + └─ Text "This is the thing that is messed with." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testBreakDescriptionWithBlankLine() { + let source = """ + @param thing The thing. + + Messes with the thing. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + ├─ DoxygenParameter parameter: thing + │ └─ Paragraph + │ └─ Text "The thing." + └─ Paragraph + └─ Text "Messes with the thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testBreakDescriptionWithOtherCommand() { + let source = """ + Messes with the thing. + + @param thing The thing. + @param otherThing The other thing. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + ├─ Paragraph + │ └─ Text "Messes with the thing." + ├─ DoxygenParameter parameter: thing + │ └─ Paragraph + │ └─ Text "The thing." + └─ DoxygenParameter parameter: otherThing + └─ Paragraph + └─ Text "The other thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testBreakDescriptionWithBlockDirective() { + let source = """ + Messes with the thing. + + @param thing The thing. + @Comment { + This is supposed to be different from the above command. + } + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + ├─ Paragraph + │ └─ Text "Messes with the thing." + ├─ DoxygenParameter parameter: thing + │ └─ Paragraph + │ └─ Text "The thing." + └─ BlockDirective name: "Comment" + └─ Paragraph + └─ Text "This is supposed to be different from the above command." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testCommandBreaksParagraph() { + let source = """ + This is a paragraph. + @param thing The thing. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + ├─ Paragraph + │ └─ Text "This is a paragraph." + └─ DoxygenParameter parameter: thing + └─ Paragraph + └─ Text "The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testSourceLocations() { + let source = """ + @param thing The thing. + This is the thing that is messed with. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document @1:1-2:39 + └─ DoxygenParameter @1:1-2:39 parameter: thing + └─ Paragraph @1:14-2:39 + ├─ Text @1:14-1:24 "The thing." + ├─ SoftBreak + └─ Text @2:1-2:39 "This is the thing that is messed with." + """ + XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump) + } + + func testSourceLocationsWithIndentation() { + let source = """ + @param thing The thing. + This is the thing that is messed with. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document @1:1-2:43 + └─ DoxygenParameter @1:1-2:43 parameter: thing + └─ Paragraph @1:14-2:43 + ├─ Text @1:14-1:24 "The thing." + ├─ SoftBreak + └─ Text @2:5-2:43 "This is the thing that is messed with." + """ + XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump) + } + + func testSourceLocationsWithIndentedAtSign() { + let source = """ + Method description. + + @param thing The thing. + This is the thing that is messed with. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document @1:1-4:43 + ├─ Paragraph @1:1-1:20 + │ └─ Text @1:1-1:20 "Method description." + └─ DoxygenParameter @3:2-4:43 parameter: thing + └─ Paragraph @3:15-4:43 + ├─ Text @3:15-3:25 "The thing." + ├─ SoftBreak + └─ Text @4:5-4:43 "This is the thing that is messed with." + """ + XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump) + } + + func testDoesNotParseWithoutOption() { + do { + let source = """ + @param thing The thing. + """ + + let document = Document(parsing: source, options: .parseBlockDirectives) + + let expectedDump = """ + Document + └─ BlockDirective name: "param" + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + do { + let source = """ + @param thing The thing. + """ + + let document = Document(parsing: source) + + let expectedDump = """ + Document + └─ Paragraph + └─ Text "@param thing The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + } + + func testDoesNotParseInsideBlockDirective() { + let source = """ + @Comment { + @param thing The thing. + } + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + └─ BlockDirective name: "Comment" + └─ BlockDirective name: "param" + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + func testDoesNotParseInsideCodeBlock() { + do { + let source = """ + ``` + @param thing The thing. + ``` + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + └─ CodeBlock language: none + @param thing The thing. + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + do { + let source = """ + Paragraph to set indentation. + + @param thing The thing. + """ + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = """ + Document + ├─ Paragraph + │ └─ Text "Paragraph to set indentation." + └─ CodeBlock language: none + @param thing The thing. + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + } + + func testDoesNotParseUnknownCommand() { + let source = #""" + \unknown + """# + + let document = Document(parsing: source, options: parseOptions) + + let expectedDump = #""" + Document + └─ Paragraph + └─ Text "\unknown" + """# + XCTAssertEqual(document.debugDescription(), expectedDump) + } +} diff --git a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift index a9f6a155..bb4a9f1a 100644 --- a/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift +++ b/Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2023 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 @@ -288,6 +288,44 @@ class MarkupFormatterSingleElementTests: XCTestCase { let printed = SymbolLink(destination: "foo()").format() XCTAssertEqual(expected, printed) } + + func testPrintDoxygenParameter() { + let expected = #"\param thing The thing."# + let printed = DoxygenParameter(name: "thing", children: Paragraph(Text("The thing."))).format() + XCTAssertEqual(expected, printed) + } + + func testPrintDoxygenParameterMultiline() { + let expected = #""" + \param thing The thing. + This is an extended discussion. + """# + let printed = DoxygenParameter(name: "thing", children: Paragraph( + Text("The thing."), + SoftBreak(), + Text("This is an extended discussion.") + )).format() + XCTAssertEqual(expected, printed) + } + + func testPrintDoxygenReturns() { + let expected = #"\returns Another thing."# + let printed = DoxygenReturns(children: Paragraph(Text("Another thing."))).format() + XCTAssertEqual(expected, printed) + } + + func testPrintDoxygenReturnsMultiline() { + let expected = #""" + \returns Another thing. + This is an extended discussion. + """# + let printed = DoxygenReturns(children: Paragraph( + Text("Another thing."), + SoftBreak(), + Text("This is an extended discussion.") + )).format() + XCTAssertEqual(expected, printed) + } } /// Tests that formatting options work correctly. @@ -502,6 +540,23 @@ class MarkupFormatterOptionsTests: XCTestCase { XCTAssertEqual(incrementing, printed) } } + + func testDoxygenCommandPrefix() { + let backslash = #"\param thing The thing."# + let at = "@param thing The thing." + + do { + let document = Document(parsing: backslash, options: [.parseMinimalDoxygen, .parseBlockDirectives]) + let printed = document.format(options: .init(doxygenCommandPrefix: .at)) + XCTAssertEqual(at, printed) + } + + do { + let document = Document(parsing: at, options: [.parseMinimalDoxygen, .parseBlockDirectives]) + let printed = document.format(options: .init(doxygenCommandPrefix: .backslash)) + XCTAssertEqual(backslash, printed) + } + } } /// Tests that an printed and reparsed element has the same structure as @@ -679,7 +734,8 @@ class MarkupFormatterSimpleRoundTripTests: XCTestCase { let document = try Document(parsing: readMeURL) // try document.format().write(toFile: "/tmp/test.md", atomically: true, encoding: .utf8) checkRoundTrip(for: document) - }} + } +} /** Test enforcement of a preferred maximum line length. diff --git a/bin/check-source b/bin/check-source index e1b11285..26717a7d 100755 --- a/bin/check-source +++ b/bin/check-source @@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/20[12][78901]-20[12][89012]/YEARS/' -e 's/20[12][89012]/YEARS/' + sed -e 's/20[12][7890123]-20[12][890123]/YEARS/' -e 's/20[12][890123]/YEARS/' } printf "=> Checking for unacceptable language… " @@ -40,7 +40,7 @@ printf "\033[0;32mokay.\033[0m\n" printf "=> Checking license headers… " tmp=$(mktemp /tmp/.swift-markdown-check-source_XXXXXX) -for language in swift-or-c bash md-or-tutorial html docker; do +for language in swift-or-c snippet bash md-or-tutorial html docker; do declare -a matching_files declare -a exceptions declare -a reader @@ -49,7 +49,7 @@ for language in swift-or-c bash md-or-tutorial html docker; do reader=head case "$language" in swift-or-c) - exceptions=( -name 'Package*.swift') + exceptions=( -name 'Package*.swift' -o -path './Snippets/*') matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) cat > "$tmp" <<"EOF" /* @@ -61,6 +61,22 @@ for language in swift-or-c bash md-or-tutorial html docker; do See https://swift.org/LICENSE.txt for license information See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ +EOF + ;; + snippet) + matching_files=( -name '*.swift' -a -path './Snippets/*') + exceptions=( -name 'Package*.swift') + reader=tail + cat > "$tmp" <<"EOF" +/* + This source file is part of the Swift.org open source project + + Copyright (c) YEARS 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 Swift project authors +*/ EOF ;; bash) @@ -150,3 +166,4 @@ done printf "\033[0;32mokay.\033[0m\n" rm "$tmp" +# vim: filetype=bash shiftwidth=2 softtabstop=2