├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MarkdownKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── MarkdownKit iOS.xcscheme │ ├── MarkdownKit.xcscheme │ └── MarkdownKitProcess.xcscheme ├── MarkdownKitDemo.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ └── xcschemes │ └── Playground.xcscheme ├── MarkdownKitPlayground.playground ├── Contents.swift ├── Resources │ └── Demo.md ├── Sources │ └── MarkdownPlayground.swift └── contents.xcplayground ├── Package.swift ├── README.md ├── Sources ├── MarkdownKit │ ├── AttributedString │ │ ├── AttributedStringGenerator.swift │ │ └── Color.swift │ ├── Block.swift │ ├── Blocks.swift │ ├── CustomBlock.swift │ ├── CustomTextFragment.swift │ ├── HTML │ │ ├── HtmlGenerator.swift │ │ ├── NamedCharacters.swift │ │ └── String+Entities.swift │ ├── Info.plist │ ├── MarkdownKit.h │ ├── Parser │ │ ├── AtxHeadingParser.swift │ │ ├── BlockParser.swift │ │ ├── BlockquoteParser.swift │ │ ├── CodeBlockParser.swift │ │ ├── CodeLinkHtmlTransformer.swift │ │ ├── Container.swift │ │ ├── DelimiterTransformer.swift │ │ ├── DocumentParser.swift │ │ ├── EmphasisTransformer.swift │ │ ├── EscapeTransformer.swift │ │ ├── ExtendedDocumentParser.swift │ │ ├── ExtendedListItemParser.swift │ │ ├── ExtendedMarkdownParser.swift │ │ ├── HtmlBlockParser.swift │ │ ├── InlineParser.swift │ │ ├── InlineTransformer.swift │ │ ├── LinkRefDefinitionParser.swift │ │ ├── LinkTransformer.swift │ │ ├── ListItemParser.swift │ │ ├── MarkdownParser.swift │ │ ├── ParserUtil.swift │ │ ├── SetextHeadingParser.swift │ │ ├── TableParser.swift │ │ └── ThematicBreakParser.swift │ ├── Text.swift │ └── TextFragment.swift └── MarkdownKitProcess │ └── main.swift └── Tests ├── LinuxMain.swift └── MarkdownKitTests ├── ExtendedMarkdownBlockTests.swift ├── ExtendedMarkdownHtmlTests.swift ├── Info.plist ├── MarkdownASTests.swift ├── MarkdownBlockTests.swift ├── MarkdownExtension.swift ├── MarkdownFactory.swift ├── MarkdownHtmlTests.swift ├── MarkdownInlineTests.swift └── MarkdownStringTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | *.xcuserstate 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | Packages 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots/**/*.png 70 | fastlane/test_output 71 | 72 | # Code Injection 73 | # 74 | # After new code Injection tools there's a generated folder /iOSInjectionProject 75 | # https://github.com/johnno1962/injectionforxcode 76 | 77 | iOSInjectionProject/ 78 | 79 | # macOS 80 | .DS_Store 81 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2 (2025-03-28) 4 | 5 | - Improve the generation of nested lists for `AttributedStringGenerator` 6 | - Introduce a new option `tightLists` for `AttributedStringGenerator` which renders lists more compactly 7 | - The API of `AttributedStringGenerator` changes with this release 8 | 9 | ## 1.1.9 (2024-08-04) 10 | 11 | - Support converting Markdown into a string without any markup 12 | 13 | ## 1.1.8 (2024-05-01) 14 | - Fix `Color.hexString` on iOS to handle black correctly 15 | - Clean up `Package.swift` 16 | - Updated `CHANGELOG` 17 | 18 | ## 1.1.7 (2023-04-10) 19 | - Fix handling of copyright sign when escaped as XML named character 20 | 21 | ## 1.1.6 (2023-04-10) 22 | - Migrate framework to Xcode 14 23 | - Fix tests related to images in attributed strings 24 | 25 | ## 1.1.5 (2022-02-27) 26 | - Bug fixes to make `AttributedStringGenerator` work with images. 27 | 28 | ## 1.1.4 (2022-02-27) 29 | - Allow customization of image sizes in the `AttributedStringGenerator` 30 | - Support relative image links in the `AttributedStringGenerator` 31 | 32 | ## 1.1.3 (2022-02-07) 33 | - Fix build breakage for Linux 34 | - Encode predefined XML entities also for code blocks 35 | - Migrate framework to Xcode 13 36 | 37 | ## 1.1.2 (2021-06-30) 38 | - Allow creation of definition lists outside of MarkdownKit 39 | 40 | ## 1.1.0 (2021-05-12) 41 | - Make abstract syntax trees extensible 42 | - Provide a simple means to define new types of emphasis 43 | - Document support for definition lists via `ExtendedMarkdownParser` 44 | - Migrate framework to Xcode 12.5 45 | 46 | ## 1.0.4 (2021-02-15) 47 | - Support Linux 48 | - Fix handling of XML/HTML entities/named character references 49 | - Escape angle brackets in HTML output 50 | - Migrate project to Xcode 12.4 51 | 52 | ## 1.0.3 (2021-02-03) 53 | - Make framework available to iOS 54 | 55 | ## 1.0.2 (2020-10-04) 56 | - Improved extensibility of `AttributedStringGenerator` class 57 | 58 | ## 1.0.1 (2020-10-04) 59 | - Ported to Swift 5.3 60 | - Migrated project to Xcode 12.0 61 | 62 | ## 1.0 (2020-07-18) 63 | - Implemented support for Markdown tables 64 | - Made it easier to extend class `MarkdownParser` 65 | - Included extended markdown parser `ExtendedMarkdownParser` 66 | 67 | ## 0.2.2 (2020-01-26) 68 | - Fixed bug in AttributedStringGenerator.swift 69 | - Migrated project to Xcode 11.3.1 70 | 71 | ## 0.2.1 (2019-12-28) 72 | - Simplified extension/usage of NSAttributedString generator 73 | - Migrated project to Xcode 11.3 74 | 75 | ## 0.2 (2019-10-19) 76 | - Implemented support for backslash escaping 77 | - Added support for using link reference definitions; not fully CommonMark-compliant yet 78 | - Migrated project to Xcode 11.1 79 | 80 | ## 0.1 (2019-08-17) 81 | - Initial version 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it again. 16 | 17 | ## Code reviews 18 | 19 | All submissions, including submissions by project members, require review. We 20 | use GitHub pull requests for this purpose. Consult 21 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 22 | information on using pull requests. 23 | 24 | ## Community Guidelines 25 | 26 | This project follows [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 27 | -------------------------------------------------------------------------------- /MarkdownKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MarkdownKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MarkdownKit.xcodeproj/xcshareddata/xcschemes/MarkdownKit iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /MarkdownKit.xcodeproj/xcshareddata/xcschemes/MarkdownKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /MarkdownKit.xcodeproj/xcshareddata/xcschemes/MarkdownKitProcess.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /MarkdownKitDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MarkdownKitDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MarkdownKitDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MarkdownKitDemo.xcworkspace/xcshareddata/xcschemes/Playground.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /MarkdownKitPlayground.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownKitPlayground 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 01/08/2019. 6 | // Copyright © 2019-2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, 15 | // software distributed under the License is distributed on an 16 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 17 | // either express or implied. See the License for the specific 18 | // language governing permissions and limitations under the 19 | // License. 20 | // 21 | 22 | import PlaygroundSupport 23 | 24 | PlaygroundPage.current.liveView = 25 | markdownView(file: "Demo", width: 500, height: 850) 26 | -------------------------------------------------------------------------------- /MarkdownKitPlayground.playground/Resources/Demo.md: -------------------------------------------------------------------------------- 1 | # Sample document 2 | ## A few lines of Markdown text 3 | 4 | **(make-drawing)** 5 | 6 | Returns a new, empty drawing. A _drawing_ consists of a sequence of drawing instructions and 7 | drawing state consisting of the following components: 8 | 9 | - Stroke color (set via `set-color`) 10 | - Fill color (set via `fill-color`) 11 | - Shadow (set via `set-shadow` and `remove-shadow`) 12 | - Transformation (add transformation via `enable-transformation` and remove via `disable-transformation`) 13 | 14 | *** 15 | 16 | **(enum-set-indexer _enum-set_)** 17 | 18 | Returns a unary procedure that, given a symbol that is in the universe of _enum-set_, 19 | returns its 0-origin index within the canonical ordering of the symbols in the universe; 20 | given a value not in the universe, the unary procedure returns `#f`. 21 | 22 | Returns a unary procedure that, given a symbol that is in the universe of _enum-set_, 23 | returns its 0-origin index within the canonical ordering of the symbols 24 | 25 | ``` 26 | (let* ((e (make-enumeration '(red green blue))) 27 | (i (enum-set-indexer e))) 28 | (list (i 'red) (i 'green) (i 'blue) (i 'yellow))) 29 | ⇒ (0 1 2 #f) 30 | ``` 31 | 32 | The `enum-set-indexer` procedure could be defined as follows using the new `memq` procedure. 33 | 34 | > And this is a fancy 35 | > blockquote `code block`. This requires special treatment since the HTML to NSAttributedString 36 | > conversion is not able to render blockquote tags. 37 | > 38 | > *** 39 | > 40 | > This is still in the blockquote. 41 | 42 | There is more text coming after the blockquote, including a table: 43 | 44 | | Country | Country code | Dialing code | 45 | | :----------- | :--------------: | :-------------: | 46 | | Albania | AL | +355 | 47 | | Argentina | AR | +54 | 48 | | Austria | AT | +43 | 49 | | Switzerland | CH | +41 | 50 | 51 | Description lists also need special treatment when converted to `NSAttributedString`: 52 | 53 | One 54 | : This is the description for _One_ 55 | 56 | Two 57 | : This is the description for _Two_ 58 | 59 | -------------------------------------------------------------------------------- /MarkdownKitPlayground.playground/Sources/MarkdownPlayground.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownPlayground.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 02/08/2019. 6 | // Copyright © 2019-2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | import AppKit 23 | import PlaygroundSupport 24 | import MarkdownKit 25 | 26 | public class MarkdownView: NSView { 27 | let str: NSAttributedString 28 | let rect: NSRect 29 | 30 | init(str: NSAttributedString?, width: Double, height: Double) { 31 | self.str = str ?? NSAttributedString(string: "") 32 | self.rect = NSRect(x: 0.0, y: 0.0, width: width, height: height) 33 | super.init(frame: self.rect) 34 | } 35 | 36 | required init?(coder decoder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | override public func draw(_ dirtyRect: NSRect) { 41 | str.draw(in: rect) 42 | } 43 | } 44 | 45 | public func loadText(file: String) -> String? { 46 | if let fileUrl = Bundle.main.url(forResource: file, withExtension: "md") { 47 | return try? String(contentsOf: fileUrl, encoding: String.Encoding.utf8) 48 | } else { 49 | return nil 50 | } 51 | } 52 | 53 | public func markdownView(text: String, width: Double, height: Double) -> NSView { 54 | let markdown = ExtendedMarkdownParser.standard.parse(text) 55 | return MarkdownView(str: AttributedStringGenerator.standard.generate(doc: markdown), 56 | width: width, 57 | height: height) 58 | } 59 | 60 | public func markdownView(file: String, width: Double, height: Double) -> NSView { 61 | return markdownView(text: loadText(file: file) ?? "", 62 | width: width, 63 | height: height) 64 | } 65 | -------------------------------------------------------------------------------- /MarkdownKitPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | // 3 | // Package.swift 4 | // MarkdownKit 5 | // 6 | // Build targets by calling the Swift Package Manager in the following way for debug purposes: 7 | // `swift build` 8 | // 9 | // A release can be built with these options: 10 | // `swift build -c release` 11 | // 12 | // The tests can be executed via: 13 | // `swift test` 14 | // 15 | // Created by Matthias Zenger on 09/08/2019. 16 | // Copyright © 2019-2025 Google LLC. 17 | // 18 | // Licensed under the Apache License, Version 2.0 (the "License"); 19 | // you may not use this file except in compliance with the License. 20 | // You may obtain a copy of the License at 21 | // 22 | // http://www.apache.org/licenses/LICENSE-2.0 23 | // 24 | // Unless required by applicable law or agreed to in writing, software 25 | // distributed under the License is distributed on an "AS IS" BASIS, 26 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | // See the License for the specific language governing permissions and 28 | // limitations under the License. 29 | // 30 | 31 | import PackageDescription 32 | 33 | let package = Package( 34 | name: "MarkdownKit", 35 | platforms: [ 36 | .macOS(.v10_12), 37 | .iOS(.v13), 38 | .tvOS(.v13), 39 | .watchOS(.v6) 40 | ], 41 | products: [ 42 | .library(name: "MarkdownKit", targets: ["MarkdownKit"]), 43 | .executable(name: "MarkdownKitProcess", targets: ["MarkdownKitProcess"]) 44 | ], 45 | dependencies: [ 46 | ], 47 | targets: [ 48 | .target(name: "MarkdownKit", 49 | dependencies: [], 50 | exclude: ["Info.plist"]), 51 | .executableTarget(name: "MarkdownKitProcess", 52 | dependencies: ["MarkdownKit"], 53 | exclude: []), 54 | .testTarget(name: "MarkdownKitTests", 55 | dependencies: ["MarkdownKit"], 56 | exclude: ["Info.plist"]) 57 | ], 58 | swiftLanguageVersions: [.v5] 59 | ) 60 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/AttributedString/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 18/08/2019. 6 | // Copyright © 2019-2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | #if os(iOS) || os(watchOS) || os(tvOS) 22 | 23 | import UIKit 24 | 25 | extension UIColor { 26 | 27 | public var hexString: String { 28 | guard let components = self.cgColor.components else { 29 | return "#FFFFFF" 30 | } 31 | var red = 0 32 | var green = 0 33 | var blue = 0 34 | if components.count >= 3 { 35 | red = Int(round(components[0] * 0xff)) 36 | green = Int(round(components[1] * 0xff)) 37 | blue = Int(round(components[2] * 0xff)) 38 | } else if components.count >= 1 { 39 | red = Int(round(components[0] * 0xff)) 40 | green = Int(round(components[0] * 0xff)) 41 | blue = Int(round(components[0] * 0xff)) 42 | } 43 | return String(format: "#%02X%02X%02X", red, green, blue) 44 | } 45 | } 46 | 47 | #elseif os(macOS) 48 | 49 | import Cocoa 50 | 51 | extension NSColor { 52 | 53 | public var hexString: String { 54 | guard let rgb = self.usingColorSpace(NSColorSpace.deviceRGB) else { 55 | return "#FFFFFF" 56 | } 57 | let red = Int(round(rgb.redComponent * 0xff)) 58 | let green = Int(round(rgb.greenComponent * 0xff)) 59 | let blue = Int(round(rgb.blueComponent * 0xff)) 60 | return String(format: "#%02X%02X%02X", red, green, blue) 61 | } 62 | } 63 | 64 | public let mdDefaultColor = NSColor.textColor.hexString 65 | public let mdDefaultBackgroundColor = NSColor.textBackgroundColor.hexString 66 | 67 | #endif 68 | 69 | #if os(iOS) 70 | 71 | public let mdDefaultColor = UIColor.label.hexString 72 | public let mdDefaultBackgroundColor = UIColor.systemBackground.hexString 73 | 74 | #elseif os(tvOS) 75 | 76 | public let mdDefaultColor = UIColor.label.hexString 77 | public let mdDefaultBackgroundColor = UIColor.white.hexString 78 | 79 | #elseif os(watchOS) 80 | 81 | public let mdDefaultColor = UIColor.black.hexString 82 | public let mdDefaultBackgroundColor = UIColor.white.hexString 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Blocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Blocks.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 11/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A normalized sequence of blocks, represented as an array. 25 | /// 26 | public typealias Blocks = ContiguousArray 27 | 28 | extension Blocks { 29 | 30 | /// Returns the text of a singleton `Blocks` object. A singleton `Blocks` object contains a 31 | /// single paragraph. This property returns `nil` if this object is not a singleton `Blocks` 32 | /// object. 33 | public var text: Text? { 34 | if self.count == 1, 35 | case .paragraph(let text) = self[0] { 36 | return text 37 | } else { 38 | return nil 39 | } 40 | } 41 | 42 | /// Returns true if this is a singleton `Blocks` object. 43 | public var isSingleton: Bool { 44 | return self.count == 1 45 | } 46 | 47 | /// Returns raw text for this sequence of blocks. 48 | public var string: String { 49 | return self.map { $0.string }.joined(separator: "\n") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/CustomBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomBlock.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 12/05/2021. 6 | // Copyright © 2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// Protocol `CustomBlock` defines the interface of custom Markdown elements that are implemented 25 | /// externally (i.e. not by the MarkdownKit framework). 26 | /// 27 | public protocol CustomBlock: CustomStringConvertible, CustomDebugStringConvertible { 28 | var string: String { get } 29 | func equals(to other: CustomBlock) -> Bool 30 | func parse(via parser: InlineParser) -> Block 31 | func generateHtml(via htmlGen: HtmlGenerator, tight: Bool) -> String 32 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 33 | func generateHtml(via htmlGen: HtmlGenerator, 34 | and attGen: AttributedStringGenerator?, 35 | tight: Bool) -> String 36 | #endif 37 | } 38 | 39 | extension CustomBlock { 40 | /// By default, the custom block does not have any raw string content. 41 | public var string: String { 42 | return "" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/CustomTextFragment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomTextFragment.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 12/05/2021. 6 | // Copyright © 2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// Protocol `CustomTextFragment` defines the interface for custom Markdown text fragments 25 | /// that are implemented externally (i.e. not by the MarkdownKit framework). 26 | /// 27 | public protocol CustomTextFragment: CustomStringConvertible, CustomDebugStringConvertible { 28 | func equals(to other: CustomTextFragment) -> Bool 29 | func transform(via transformer: InlineTransformer) -> TextFragment 30 | func generateHtml(via htmlGen: HtmlGenerator) -> String 31 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 32 | func generateHtml(via htmlGen: HtmlGenerator, and attrGen: AttributedStringGenerator?) -> String 33 | #endif 34 | var rawDescription: String { get } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/HTML/HtmlGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HtmlGenerator.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 15/07/2019. 6 | // Copyright © 2019-2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// `HtmlGenerator` provides functionality for converting Markdown blocks into HTML. The 25 | /// implementation is extensible allowing subclasses of `HtmlGenerator` to override how 26 | /// individual Markdown structures are converted into HTML. 27 | /// 28 | open class HtmlGenerator { 29 | 30 | public enum Parent { 31 | case none 32 | indirect case block(Block, Parent) 33 | } 34 | 35 | /// Default `HtmlGenerator` implementation 36 | public static let standard = HtmlGenerator() 37 | 38 | public init() {} 39 | 40 | /// `generate` takes a block representing a Markdown document and returns a corresponding 41 | /// representation in HTML as a string. 42 | open func generate(doc: Block) -> String { 43 | guard case .document(let blocks) = doc else { 44 | preconditionFailure("cannot generate HTML from \(doc)") 45 | } 46 | return self.generate(blocks: blocks, parent: .none) 47 | } 48 | 49 | open func generate(blocks: Blocks, parent: Parent, tight: Bool = false) -> String { 50 | var res = "" 51 | for block in blocks { 52 | res += self.generate(block: block, parent: parent, tight: tight) 53 | } 54 | return res 55 | } 56 | 57 | open func generate(block: Block, parent: Parent, tight: Bool = false) -> String { 58 | switch block { 59 | case .document(_): 60 | preconditionFailure("broken block \(block)") 61 | case .blockquote(let blocks): 62 | return "
\n" + 63 | self.generate(blocks: blocks, parent: .block(block, parent)) + 64 | "
\n" 65 | case .list(let start, let tight, let blocks): 66 | if let startNumber = start { 67 | return "
    \n" + 68 | self.generate(blocks: blocks, parent: .block(block, parent), tight: tight) + 69 | "
\n" 70 | } else { 71 | return "
    \n" + 72 | self.generate(blocks: blocks, parent: .block(block, parent), tight: tight) + 73 | "
\n" 74 | } 75 | case .listItem(_, _, let blocks): 76 | if tight, let text = blocks.text { 77 | return "
  • " + self.generate(text: text) + "
  • \n" 78 | } else { 79 | return "
  • " + 80 | self.generate(blocks: blocks, parent: .block(block, parent), tight: tight) + 81 | "
  • \n" 82 | } 83 | case .paragraph(let text): 84 | if tight { 85 | return self.generate(text: text) + "\n" 86 | } else { 87 | return "

    " + self.generate(text: text) + "

    \n" 88 | } 89 | case .heading(let n, let text): 90 | let tag = "h\(n > 0 && n < 7 ? n : 1)>" 91 | return "<\(tag)\(self.generate(text: text))" + 94 | self.generate(lines: lines).encodingPredefinedXmlEntities() + 95 | "\n" 96 | case .fencedCode(let lang, let lines): 97 | if let language = lang { 98 | return "
    " +
     99 |                  self.generate(lines: lines, separator: "").encodingPredefinedXmlEntities() +
    100 |                  "
    \n" 101 | } else { 102 | return "
    " +
    103 |                  self.generate(lines: lines, separator: "").encodingPredefinedXmlEntities() +
    104 |                  "
    \n" 105 | } 106 | case .htmlBlock(let lines): 107 | return self.generate(lines: lines) 108 | case .referenceDef(_, _, _): 109 | return "" 110 | case .thematicBreak: 111 | return "
    \n" 112 | case .table(let header, let align, let rows): 113 | var tagsuffix: [String] = [] 114 | for a in align { 115 | switch a { 116 | case .undefined: 117 | tagsuffix.append(">") 118 | case .left: 119 | tagsuffix.append(" align=\"left\">") 120 | case .right: 121 | tagsuffix.append(" align=\"right\">") 122 | case .center: 123 | tagsuffix.append(" align=\"center\">") 124 | } 125 | } 126 | var html = "\n" 127 | var i = 0 128 | for head in header { 129 | html += "" 130 | i += 1 131 | } 132 | html += "\n\n" 133 | for row in rows { 134 | html += "" 135 | i = 0 136 | for cell in row { 137 | html += "" 138 | i += 1 139 | } 140 | html += "\n" 141 | } 142 | html += "
    \n" 143 | return html 144 | case .definitionList(let defs): 145 | var html = "
    \n" 146 | for def in defs { 147 | html += "
    " + self.generate(text: def.item) + "
    \n" 148 | for descr in def.descriptions { 149 | if case .listItem(_, _, let blocks) = descr { 150 | if blocks.count == 1, 151 | case .paragraph(let text) = blocks.first! { 152 | html += "
    " + self.generate(text: text) + "
    \n" 153 | } else { 154 | html += "
    " + 155 | self.generate(blocks: blocks, parent: .block(block, parent)) + 156 | "
    \n" 157 | } 158 | } 159 | } 160 | } 161 | html += "
    \n" 162 | return html 163 | case .custom(let customBlock): 164 | return customBlock.generateHtml(via: self, tight: tight) 165 | } 166 | } 167 | 168 | open func generate(text: Text) -> String { 169 | var res = "" 170 | for fragment in text { 171 | res += self.generate(textFragment: fragment) 172 | } 173 | return res 174 | } 175 | 176 | open func generate(textFragment fragment: TextFragment) -> String { 177 | switch fragment { 178 | case .text(let str): 179 | return String(str).decodingNamedCharacters().encodingPredefinedXmlEntities() 180 | case .code(let str): 181 | return "" + String(str).encodingPredefinedXmlEntities() + "" 182 | case .emph(let text): 183 | return "" + self.generate(text: text) + "" 184 | case .strong(let text): 185 | return "" + self.generate(text: text) + "" 186 | case .link(let text, let uri, let title): 187 | let titleAttr = title == nil ? "" : " title=\"\(title!)\"" 188 | return "" + self.generate(text: text) + "" 189 | case .autolink(let type, let str): 190 | switch type { 191 | case .uri: 192 | return "\(str)" 193 | case .email: 194 | return "\(str)" 195 | } 196 | case .image(let text, let uri, let title): 197 | let titleAttr = title == nil ? "" : " title=\"\(title!)\"" 198 | if let uri = uri { 199 | return "\"\(text.rawDescription)\"\(titleAttr)/" 200 | } else { 201 | return self.generate(text: text) 202 | } 203 | case .html(let tag): 204 | return "<\(tag.description)>" 205 | case .delimiter(let ch, let n, _): 206 | let char: String 207 | switch ch { 208 | case "<": 209 | char = "<" 210 | case ">": 211 | char = ">" 212 | default: 213 | char = String(ch) 214 | } 215 | var res = char 216 | for _ in 1.." 224 | case .custom(let customTextFragment): 225 | return customTextFragment.generateHtml(via: self) 226 | } 227 | } 228 | 229 | open func generate(lines: Lines, separator: String = "\n") -> String { 230 | var res = "" 231 | for line in lines { 232 | if res.isEmpty { 233 | res = String(line) 234 | } else { 235 | res += separator + line 236 | } 237 | } 238 | return res 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/HTML/String+Entities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Entities.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 13/02/2021. 6 | // Copyright © 2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | extension String { 24 | 25 | public func encodingPredefinedXmlEntities() -> String { 26 | var res = "" 27 | var pos = self.startIndex 28 | // find the first character that requires encoding 29 | while pos < self.endIndex, 30 | let index = self.rangeOfCharacter(from: Self.predefinedEntities, 31 | range: pos..": 45 | res.append(contentsOf: ">") 46 | default: 47 | res.append(self[index.lowerBound]) 48 | } 49 | pos = self.index(after: index.lowerBound) 50 | } 51 | if res.isEmpty { 52 | return self 53 | } else { 54 | res.append(contentsOf: self[pos.. String { 60 | var res = "" 61 | for ch in self { 62 | if let charRef = NamedCharacters.characterNameMap[ch] { 63 | res.append(contentsOf: charRef) 64 | } else { 65 | res.append(ch) 66 | } 67 | } 68 | return res 69 | } 70 | 71 | public func decodingNamedCharacters() -> String { 72 | var res = "" 73 | var pos = self.startIndex 74 | // find the next `&` 75 | while let ampPos = self.range(of: "&", range: pos..") 109 | return set 110 | }() 111 | } 112 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2019-2025 Google LLC. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/MarkdownKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownKit.h 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 03/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | #import 22 | 23 | //! Project version number for MarkdownKit. 24 | FOUNDATION_EXPORT double MarkdownKitVersionNumber; 25 | 26 | //! Project version string for MarkdownKit. 27 | FOUNDATION_EXPORT const unsigned char MarkdownKitVersionString[]; 28 | 29 | // In this header, you should import all the public headers of your framework using statements like #import 30 | 31 | 32 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/AtxHeadingParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ATXHeadingParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 01/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A block parser which parses ATX headings (of the form `## Header`) returning `heading` blocks. 25 | /// 26 | open class AtxHeadingParser: BlockParser { 27 | 28 | public override func parse() -> ParseResult { 29 | guard self.shortLineIndent else { 30 | return .none 31 | } 32 | var i = self.contentStartIndex 33 | var level = 0 34 | while i < self.contentEndIndex && self.line[i] == "#" && level < 7 { 35 | i = self.line.index(after: i) 36 | level += 1 37 | } 38 | guard level > 0 && level < 7 && (i >= self.contentEndIndex || self.line[i] == " ") else { 39 | return .none 40 | } 41 | while i < self.contentEndIndex && self.line[i] == " " { 42 | i = self.line.index(after: i) 43 | } 44 | guard i < self.contentEndIndex else { 45 | let res: Block = .heading(level, Text(self.line[i.. i && self.line[e] == " " { 51 | e = self.line.index(before: e) 52 | } 53 | if e > i && self.line[e] == "#" { 54 | let e0 = e 55 | while e > i && self.line[e] == "#" { 56 | e = self.line.index(before: e) 57 | } 58 | if e >= i && self.line[e] == " " { 59 | while e >= i && self.line[e] == " " { 60 | e = self.line.index(before: e) 61 | } 62 | } else { 63 | e = e0 64 | } 65 | } 66 | let res: Block = .heading(level, Text(self.line[i...e])) 67 | self.readNextLine() 68 | return .block(res) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/BlockParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 01/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A `BlockParser` parses one particular type of Markdown blocks. Class `BlockParser` defines 25 | /// a framework for such block parsers. Every different block type comes with its own subclass 26 | /// of `BlockParser`. 27 | /// 28 | open class BlockParser { 29 | 30 | /// The result of calling the `parse` method. 31 | public enum ParseResult { 32 | case none 33 | case block(Block) 34 | case container((Container) -> Container) 35 | } 36 | 37 | unowned let docParser: DocumentParser 38 | 39 | public required init(docParser: DocumentParser) { 40 | self.docParser = docParser 41 | } 42 | 43 | public var finished: Bool { 44 | return self.docParser.finished 45 | } 46 | 47 | public var prevParagraphLines: Text? { 48 | return self.docParser.prevParagraphLines 49 | } 50 | 51 | public func consumeParagraphLines() { 52 | self.docParser.prevParagraphLines = nil 53 | self.docParser.prevParagraphLinesTight = false 54 | } 55 | 56 | public var line: Substring { 57 | return self.docParser.line 58 | } 59 | 60 | public var contentStartIndex: Substring.Index { 61 | return self.docParser.contentStartIndex 62 | } 63 | 64 | public var contentEndIndex: Substring.Index { 65 | return self.docParser.contentEndIndex 66 | } 67 | 68 | public var lineIndent: Int { 69 | return self.docParser.lineIndent 70 | } 71 | 72 | public var lineEmpty: Bool { 73 | return self.docParser.lineEmpty 74 | } 75 | 76 | public var prevLineEmpty: Bool { 77 | return self.docParser.prevLineEmpty 78 | } 79 | 80 | public var shortLineIndent: Bool { 81 | return self.docParser.shortLineIndent 82 | } 83 | 84 | public var lazyContinuation: Bool { 85 | return self.docParser.lazyContinuation 86 | } 87 | 88 | open func readNextLine() { 89 | self.docParser.readNextLine() 90 | } 91 | 92 | open var mayInterruptParagraph: Bool { 93 | return true 94 | } 95 | 96 | open func parse() -> ParseResult { 97 | return .none 98 | } 99 | } 100 | 101 | /// 102 | /// `RestorableBlockParser` objects are `BlockParser` objects which restore the 103 | /// `DocumentParser` state in case their `parse` method fails (the `ParseResult` is `.none`). 104 | /// 105 | open class RestorableBlockParser: BlockParser { 106 | private var docParserState: DocumentParserState 107 | 108 | public required init(docParser: DocumentParser) { 109 | self.docParserState = DocumentParserState(docParser) 110 | super.init(docParser: docParser) 111 | } 112 | 113 | open override func parse() -> ParseResult { 114 | self.docParser.copyState(&self.docParserState) 115 | let res = self.tryParse() 116 | if case .none = res { 117 | self.docParser.restoreState(self.docParserState) 118 | return .none 119 | } else { 120 | return res 121 | } 122 | } 123 | 124 | open func tryParse() -> ParseResult { 125 | return .none 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/BlockquoteParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockquoteParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 03/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A block parser which parses block quotes eturning `blockquote` blocks. 25 | /// 26 | open class BlockquoteParser: BlockParser { 27 | 28 | private final class BlockquoteContainer: NestedContainer { 29 | 30 | public override var indentRequired: Bool { 31 | return true 32 | } 33 | 34 | public override func skipIndent(input: String, 35 | startIndex: String.Index, 36 | endIndex: String.Index) -> String.Index? { 37 | var index = startIndex 38 | var indent = 0 39 | while index < endIndex && input[index] == " " { 40 | indent += 1 41 | index = input.index(after: index) 42 | } 43 | guard indent < 4 && index < endIndex && input[index] == ">" else { 44 | return nil 45 | } 46 | index = input.index(after: index) 47 | if index < endIndex && input[index] == " " { 48 | index = input.index(after: index) 49 | } 50 | return index 51 | } 52 | 53 | public override func makeBlock(_ docParser: DocumentParser) -> Block { 54 | return .blockquote(docParser.bundle(blocks: self.content)) 55 | } 56 | 57 | public override var debugDescription: String { 58 | return self.outer.debugDescription + " <- blockquote" 59 | } 60 | } 61 | 62 | public override func parse() -> ParseResult { 63 | guard self.shortLineIndent && self.line[self.contentStartIndex] == ">" else { 64 | return .none 65 | } 66 | let i = self.line.index(after: self.contentStartIndex) 67 | if i < self.contentEndIndex && self.line[i] == " " { 68 | self.docParser.resetLineStart(self.line.index(after: i)) 69 | } else { 70 | self.docParser.resetLineStart(i) 71 | } 72 | return .container(BlockquoteContainer.init) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/CodeBlockParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeBlockParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 01/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// Block parsers for parsing different types of code blocks. `CodeBlockParser` implements 25 | /// shared logic between two concrete implementations, `IndentedCodeBlockParser` and 26 | /// `FencedCodeBlockParser`. 27 | /// 28 | open class CodeBlockParser: BlockParser { 29 | 30 | public func formatIndentedLine(_ n: Int = 4) -> Substring { 31 | var index = self.line.startIndex 32 | var indent = 0 33 | while index < self.line.endIndex && indent < n { 34 | if self.line[index] == " " { 35 | indent += 1 36 | } else if self.line[index] == "\t" { 37 | indent += 4 38 | } else { 39 | break 40 | } 41 | index = self.line.index(after: index) 42 | } 43 | return self.line[index.. ParseResult { 57 | guard !self.shortLineIndent else { 58 | return .none 59 | } 60 | var code: Lines = [self.formatIndentedLine()] 61 | var emptyLines: Lines = [] 62 | self.readNextLine() 63 | while !self.finished && self.lineEmpty { 64 | self.readNextLine() 65 | } 66 | while !self.finished && (!self.shortLineIndent || self.lineEmpty) { 67 | if self.lineEmpty { 68 | emptyLines.append(self.formatIndentedLine()) 69 | } else { 70 | if emptyLines.count > 0 { 71 | code.append(contentsOf: emptyLines) 72 | emptyLines.removeAll() 73 | } 74 | code.append(self.formatIndentedLine()) 75 | } 76 | self.readNextLine() 77 | } 78 | return .block(.indentedCode(code)) 79 | } 80 | } 81 | 82 | /// 83 | /// A code block parser which parses fenced code blocks returning `fencedCode` blocks. 84 | /// 85 | public final class FencedCodeBlockParser: CodeBlockParser { 86 | 87 | public override func parse() -> ParseResult { 88 | guard self.shortLineIndent else { 89 | return .none 90 | } 91 | let fenceChar = self.line[self.contentStartIndex] 92 | guard fenceChar == "`" || fenceChar == "~" else { 93 | return .none 94 | } 95 | let fenceIndent = self.lineIndent 96 | var fenceLength = 1 97 | var index = self.line.index(after: self.contentStartIndex) 98 | while index < self.contentEndIndex && self.line[index] == fenceChar { 99 | fenceLength += 1 100 | index = self.line.index(after: index) 101 | } 102 | guard fenceLength >= 3 else { 103 | return .none 104 | } 105 | let info = self.line[index..= fenceLength { 121 | while index < self.contentEndIndex && isUnicodeWhitespace(self.line[index]) { 122 | index = self.line.index(after: index) 123 | } 124 | if index == self.contentEndIndex { 125 | break 126 | } 127 | } 128 | } 129 | code.append(self.formatIndentedLine(fenceIndent)) 130 | self.readNextLine() 131 | } 132 | self.readNextLine() 133 | return .block(.fencedCode(info.isEmpty ? nil : info, code)) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/CodeLinkHtmlTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeLinkHtmlTransformer.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 09/06/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// An inline transformer which extracts code spans, auto-links and html tags and transforms 25 | /// them into `code`, `autolinks`, and `html` text fragments. 26 | /// 27 | open class CodeLinkHtmlTransformer: InlineTransformer { 28 | 29 | public override func transform(_ text: Text) -> Text { 30 | var res: Text = Text() 31 | var iterator = text.makeIterator() 32 | var element = iterator.next() 33 | loop: while let fragment = element { 34 | switch fragment { 35 | case .delimiter("`", let n, []): 36 | var scanner = iterator 37 | var next = scanner.next() 38 | var count = 0 39 | while let lookahead = next { 40 | count += 1 41 | switch lookahead { 42 | case .delimiter("`", n, _): 43 | var scanner2 = iterator 44 | var code = "" 45 | for _ in 1..", n, _): 70 | var scanner2 = iterator 71 | var content = "" 72 | for _ in 1.. Block { 40 | return .document(docParser.bundle(blocks: self.content)) 41 | } 42 | 43 | internal func parseIndent(input: String, 44 | startIndex: String.Index, 45 | endIndex: String.Index) -> (String.Index, Container) { 46 | return (startIndex, self) 47 | } 48 | 49 | internal func outermostIndentRequired(upto: Container) -> Container? { 50 | return nil 51 | } 52 | 53 | internal func `return`(to container: Container? = nil, for: DocumentParser) -> Container { 54 | return self 55 | } 56 | 57 | open var debugDescription: String { 58 | return "doc" 59 | } 60 | } 61 | 62 | /// 63 | /// A `NestedContainer` represents a container that has an "outer" container. 64 | /// 65 | open class NestedContainer: Container { 66 | internal let outer: Container 67 | 68 | public init(outer: Container) { 69 | self.outer = outer 70 | } 71 | 72 | open var indentRequired: Bool { 73 | return false 74 | } 75 | 76 | open func skipIndent(input: String, 77 | startIndex: String.Index, 78 | endIndex: String.Index) -> String.Index? { 79 | return startIndex 80 | } 81 | 82 | open override func makeBlock(_ docParser: DocumentParser) -> Block { 83 | preconditionFailure("makeBlock() not defined") 84 | } 85 | 86 | internal final override func parseIndent(input: String, 87 | startIndex: String.Index, 88 | endIndex: String.Index) -> (String.Index, Container) { 89 | let (index, container) = self.outer.parseIndent(input: input, 90 | startIndex: startIndex, 91 | endIndex: endIndex) 92 | guard container === self.outer else { 93 | return (index, container) 94 | } 95 | guard let res = self.skipIndent(input: input, startIndex: index, endIndex: endIndex) else { 96 | return (index, self.outer) 97 | } 98 | return (res, self) 99 | } 100 | 101 | internal final override func outermostIndentRequired(upto container: Container) -> Container? { 102 | if self === container { 103 | return nil 104 | } else if self.indentRequired { 105 | return self.outer.outermostIndentRequired(upto: container) ?? self.outer 106 | } else { 107 | return self.outer.outermostIndentRequired(upto: container) 108 | } 109 | } 110 | 111 | internal final override func `return`(to container: Container? = nil, 112 | for docParser: DocumentParser) -> Container { 113 | if self === container { 114 | return self 115 | } else { 116 | self.outer.append(block: self.makeBlock(docParser), tight: self.density?.isTight ?? true) 117 | return self.outer.return(to: container, for: docParser) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/DelimiterTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DelimiterTransformer.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 09/06/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// An inline transformer which extracts delimiters into `delimiter` text fragments. 25 | /// 26 | open class DelimiterTransformer: InlineTransformer { 27 | 28 | /// Default emphasis characters 29 | open class var emphasisChars: [Character] { 30 | return ["*", "_"] 31 | } 32 | 33 | var emphasisCharSet: Set = [] 34 | 35 | required public init(owner: InlineParser) { 36 | super.init(owner: owner) 37 | for ch in type(of: self).emphasisChars { 38 | self.emphasisCharSet.insert(ch) 39 | } 40 | } 41 | 42 | public override func transform(_ fragment: TextFragment, 43 | from iterator: inout Text.Iterator, 44 | into res: inout Text) -> TextFragment? { 45 | guard case .text(let str) = fragment else { 46 | return super.transform(fragment, from: &iterator, into: &res) 47 | } 48 | var i = str.startIndex 49 | var start = i 50 | var escape = false 51 | var split = false 52 | while i < str.endIndex { 53 | switch str[i] { 54 | case "`": 55 | var n = 1 56 | var j = str.index(after: i) 57 | while j < str.endIndex && str[j] == "`" { 58 | j = str.index(after: j) 59 | n += 1 60 | } 61 | if start < i { 62 | res.append(fragment: .text(str[start..", "[", "]", "(", ")", "\"", "'": 70 | if !escape { 71 | if start < i { 72 | res.append(fragment: .text(str[start.. str.startIndex) { 113 | if start < i { 114 | res.append(fragment: .text(str[start..= str.endIndex || 133 | isUnicodeWhitespace(str[j]) || 134 | isUnicodePunctuation(str[j])) { 135 | delimiterRunType.formUnion(.rightFlanking) 136 | if isUnicodePunctuation(str[h]) { 137 | delimiterRunType.formUnion(.leftPunctuation) 138 | } 139 | if j < str.endIndex && isUnicodePunctuation(str[j]) { 140 | delimiterRunType.formUnion(.rightPunctuation) 141 | } 142 | } 143 | } else if j < str.endIndex && !isUnicodeWhitespace(str[j]) { 144 | delimiterRunType.formUnion(.leftFlanking) 145 | } 146 | res.append(fragment: .delimiter(str[i], n,delimiterRunType)) 147 | split = true 148 | start = j 149 | i = j 150 | } else { 151 | i = str.index(after: i) 152 | escape = false 153 | } 154 | } else { 155 | i = str.index(after: i) 156 | escape = false 157 | } 158 | } 159 | } 160 | if split { 161 | if start < str.endIndex { 162 | res.append(fragment: .text(str[start...])) 163 | } 164 | } else { 165 | res.append(fragment: fragment) 166 | } 167 | return iterator.next() 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/EmphasisTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmphasisTransformer.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 16/06/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// An inline transformer which extracts emphasis markup and transforms it into `emph` and 25 | /// `strong` text fragments. 26 | /// 27 | open class EmphasisTransformer: InlineTransformer { 28 | 29 | /// Plugin specifying the type of emphasis. `ch` refers to the emphasis character, 30 | /// `special` to whether the charater is used for other use cases (e.g. "*" and "-" should 31 | /// be marked as "special"), and `factory` to a closure constructing the text fragment 32 | /// from two parameters: the first denoting whether it's double usage, and the second 33 | /// referring to the emphasized text. 34 | public struct Emphasis { 35 | public let ch: Character 36 | public let special: Bool 37 | public let factory: (Bool, Text) -> TextFragment 38 | 39 | public init(ch: Character, special: Bool, factory: @escaping (Bool, Text) -> TextFragment) { 40 | self.ch = ch 41 | self.special = special 42 | self.factory = factory 43 | } 44 | } 45 | 46 | /// Emphasis supported by default. Override this property to change what is supported. 47 | open class var supportedEmphasis: [Emphasis] { 48 | let factory = { (double: Bool, text: Text) -> TextFragment in 49 | double ? .strong(text) : .emph(text) 50 | } 51 | return [Emphasis(ch: "*", special: true, factory: factory), 52 | Emphasis(ch: "_", special: false, factory: factory)] 53 | } 54 | 55 | /// The emphasis map, used internally to determine how characters are used for emphasis 56 | /// markup. 57 | private var emphasis: [Character : Emphasis] = [:] 58 | 59 | required public init(owner: InlineParser) { 60 | super.init(owner: owner) 61 | for emph in type(of: self).supportedEmphasis { 62 | self.emphasis[emph.ch] = emph 63 | } 64 | } 65 | 66 | private struct Delimiter: CustomStringConvertible { 67 | let ch: Character 68 | let special: Bool 69 | let runType: DelimiterRunType 70 | var count: Int 71 | var index: Int 72 | 73 | init(_ ch: Character, _ special: Bool, _ rtype: DelimiterRunType, _ count: Int, _ index: Int) { 74 | self.ch = ch 75 | self.special = special 76 | self.runType = rtype 77 | self.count = count 78 | self.index = index 79 | } 80 | 81 | var isOpener: Bool { 82 | return self.runType.contains(.leftFlanking) && 83 | (self.special || 84 | !self.runType.contains(.rightFlanking) || 85 | self.runType.contains(.leftPunctuation)) 86 | } 87 | 88 | var isCloser: Bool { 89 | return self.runType.contains(.rightFlanking) && 90 | (self.special || 91 | !self.runType.contains(.leftFlanking) || 92 | self.runType.contains(.rightPunctuation)) 93 | } 94 | 95 | var countMultipleOf3: Bool { 96 | return self.count % 3 == 0 97 | } 98 | 99 | func isOpener(for ch: Character) -> Bool { 100 | return self.ch == ch && self.isOpener 101 | } 102 | 103 | func isCloser(for ch: Character) -> Bool { 104 | return self.ch == ch && self.isCloser 105 | } 106 | 107 | var description: String { 108 | return "Delimiter(\(self.ch), \(self.special), \(self.runType), \(self.count), \(self.index))" 109 | } 110 | } 111 | 112 | private typealias DelimiterStack = [Delimiter] 113 | 114 | public override func transform(_ text: Text) -> Text { 115 | // Compute delimiter stack 116 | var res: Text = Text() 117 | var iterator = text.makeIterator() 118 | var element = iterator.next() 119 | var delimiters = DelimiterStack() 120 | while let fragment = element { 121 | switch fragment { 122 | case .delimiter(let ch, let n, let type): 123 | delimiters.append(Delimiter(ch, self.emphasis[ch]?.special ?? false, type, n, res.count)) 124 | res.append(fragment: fragment) 125 | element = iterator.next() 126 | default: 127 | element = self.transform(fragment, from: &iterator, into: &res) 128 | } 129 | } 130 | self.processEmphasis(&res, &delimiters) 131 | return res 132 | } 133 | 134 | private func isSupportedEmphasisCloser(_ delimiter: Delimiter) -> Bool { 135 | for ch in self.emphasis.keys { 136 | if delimiter.isCloser(for: ch) { 137 | return true 138 | } 139 | } 140 | return false 141 | } 142 | 143 | private func processEmphasis(_ res: inout Text, _ delimiters: inout DelimiterStack) { 144 | var currentPos = 0 145 | loop: while currentPos < delimiters.count { 146 | var potentialCloser = delimiters[currentPos] 147 | if self.isSupportedEmphasisCloser(potentialCloser) { 148 | var i = currentPos - 1 149 | while i >= 0 { 150 | var potentialOpener = delimiters[i] 151 | if potentialOpener.isOpener(for: potentialCloser.ch) && 152 | ((!potentialCloser.isOpener && !potentialOpener.isCloser) || 153 | (potentialCloser.countMultipleOf3 && potentialOpener.countMultipleOf3) || 154 | ((potentialOpener.count + potentialCloser.count) % 3 != 0)) { 155 | // Deduct counts 156 | let delta = potentialOpener.count > 1 && potentialCloser.count > 1 ? 2 : 1 157 | delimiters[i].count -= delta 158 | delimiters[currentPos].count -= delta 159 | potentialOpener = delimiters[i] 160 | potentialCloser = delimiters[currentPos] 161 | // Collect fragments 162 | var nestedText = Text() 163 | for fragment in res[potentialOpener.index+1.. 0 { 169 | range.append(.delimiter(potentialOpener.ch, 170 | potentialOpener.count, 171 | potentialOpener.runType)) 172 | } 173 | if let factory = self.emphasis[potentialOpener.ch]?.factory { 174 | range.append(factory(delta > 1, nestedText)) 175 | } else { 176 | for fragment in nestedText { 177 | range.append(fragment) 178 | } 179 | } 180 | if potentialCloser.count > 0 { 181 | range.append(.delimiter(potentialCloser.ch, 182 | potentialCloser.count, 183 | potentialCloser.runType)) 184 | } 185 | let shift = range.count - potentialCloser.index + potentialOpener.index - 1 186 | res.replace(from: potentialOpener.index, to: potentialCloser.index, with: range) 187 | // Update delimiter stack 188 | if potentialCloser.count == 0 { 189 | delimiters.remove(at: currentPos) 190 | } 191 | if potentialOpener.count == 0 { 192 | delimiters.remove(at: i) 193 | currentPos -= 1 194 | } else { 195 | i += 1 196 | } 197 | var j = i 198 | while j < currentPos { 199 | delimiters.remove(at: i) 200 | j += 1 201 | } 202 | currentPos = i 203 | while i < delimiters.count { 204 | delimiters[i].index += shift 205 | i += 1 206 | } 207 | continue loop 208 | } 209 | i -= 1 210 | } 211 | if !potentialCloser.isOpener { 212 | delimiters.remove(at: currentPos) 213 | continue loop 214 | } 215 | } 216 | currentPos += 1 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/EscapeTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EscapeTransformer.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 18/10/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// An inline transformer which removes backslash escapes. 25 | /// 26 | open class EscapeTransformer: InlineTransformer { 27 | 28 | public override func transform(_ fragment: TextFragment, 29 | from iterator: inout Text.Iterator, 30 | into res: inout Text) -> TextFragment? { 31 | switch fragment { 32 | case .text(let str): 33 | res.append(fragment: .text(self.resolveEscapes(str))) 34 | case .link(let inner, let uri, let title): 35 | res.append(fragment: .link(self.transform(inner), uri, self.resolveEscapes(title))) 36 | case .image(let inner, let uri, let title): 37 | res.append(fragment: .image(self.transform(inner), uri, self.resolveEscapes(title))) 38 | default: 39 | return super.transform(fragment, from: &iterator, into: &res) 40 | } 41 | return iterator.next() 42 | } 43 | 44 | private func resolveEscapes(_ str: String?) -> String? { 45 | if let str = str { 46 | return String(self.resolveEscapes(Substring(str))) 47 | } else { 48 | return nil 49 | } 50 | } 51 | 52 | private func resolveEscapes(_ str: Substring) -> Substring { 53 | guard !str.isEmpty else { 54 | return str 55 | } 56 | var res: String? = nil 57 | var i = str.startIndex 58 | while i < str.endIndex { 59 | if str[i] == "\\" { 60 | if res == nil { 61 | res = String(str[str.startIndex.. Blocks { 32 | // First, bundle lists as previously 33 | let bundled = super.bundle(blocks: blocks) 34 | if bundled.count < 2 { 35 | return bundled 36 | } 37 | // Next, bundle lists of descriptions with their corresponding items into definition lists 38 | var res: Blocks = [] 39 | var definitions: Definitions = [] 40 | var i = 1 41 | while i < bundled.count { 42 | guard case .paragraph(let text) = bundled[i - 1], 43 | case .list(_, _, let listItems) = bundled[i], 44 | case .some(.listItem(.bullet(":"), _, _)) = listItems.first else { 45 | if definitions.count > 0 { 46 | res.append(.definitionList(definitions)) 47 | definitions.removeAll() 48 | } 49 | res.append(bundled[i - 1]) 50 | i += 1 51 | continue 52 | } 53 | definitions.append(Definition(item: text, descriptions: listItems)) 54 | i += 2 55 | } 56 | if definitions.count > 0 { 57 | res.append(.definitionList(definitions)) 58 | definitions.removeAll() 59 | } 60 | if i < bundled.count + 1 { 61 | res.append(bundled[i - 1]) 62 | } 63 | return res 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/ExtendedListItemParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtendedListItemParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 19/07/2019. 6 | // Copyright © 2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A block parser for parsing list items. There are two types of list items: 25 | /// _bullet list items_ and _ordered list items_. They are represented using `listItem` blocks 26 | /// using either the `bullet` or the `ordered list type`. `ExtendedListItemParser` also 27 | /// accepts ":" as a bullet. This is used in definition lists. 28 | /// 29 | open class ExtendedListItemParser: ListItemParser { 30 | 31 | public required init(docParser: DocumentParser) { 32 | super.init(docParser: docParser, bulletChars: ["-", "+", "*", ":"]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/ExtendedMarkdownParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtendedMarkdownParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 17/07/2020. 6 | // Copyright © 2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// `ExtendedMarkdownParser` objects are used to parse Markdown text represented as a string 25 | /// using all extensions to the CommonMark specification implemented by MarkdownKit. 26 | /// 27 | /// The `ExtendedMarkdownParser` object itself defines the configuration of the parser. 28 | /// It is stateless in the sense that it can be used for parsing many input strings. This 29 | /// is done via the `parse` function. `parse` returns an abstract syntac tree representing 30 | /// the Markdown text for the given input string. 31 | /// 32 | /// The `parse` method of the `ExtendedMarkdownParser` object delegates parsing of the input 33 | /// string to two types of processors: a `BlockParser` object and an `InlineTransformer` 34 | /// object. A `BlockParser` parses the Markdown block structure returning an abstract 35 | /// syntax tree ignoring inline markup. An `InlineTransformer` object is used to parse 36 | /// a particular type of inline markup within text of Markdown blocks, replacing the 37 | /// matching text with an abstract syntax tree representing the markup. 38 | /// 39 | /// The `parse` method of `ExtendedMarkdownParser` operates in two phases: in the first 40 | /// phase, the block structure of an input string is identified via the `BlockParser`s. 41 | /// In the second phase, the block structure gets traversed and markup within raw text 42 | /// gets replaced with a structured representation. 43 | /// 44 | open class ExtendedMarkdownParser: MarkdownParser { 45 | 46 | /// The default list of block parsers. The order of this list matters. 47 | override open class var defaultBlockParsers: [BlockParser.Type] { 48 | return self.blockParsers 49 | } 50 | 51 | private static let blockParsers: [BlockParser.Type] = MarkdownParser.headingParsers + [ 52 | IndentedCodeBlockParser.self, 53 | FencedCodeBlockParser.self, 54 | HtmlBlockParser.self, 55 | LinkRefDefinitionParser.self, 56 | BlockquoteParser.self, 57 | ExtendedListItemParser.self, 58 | TableParser.self 59 | ] 60 | 61 | /// Defines a default implementation 62 | override open class var standard: ExtendedMarkdownParser { 63 | return self.singleton 64 | } 65 | 66 | private static let singleton: ExtendedMarkdownParser = ExtendedMarkdownParser() 67 | 68 | /// Factory method to customize document parsing in subclasses. 69 | open override func documentParser(blockParsers: [BlockParser.Type], 70 | input: String) -> DocumentParser { 71 | return ExtendedDocumentParser(blockParsers: blockParsers, input: input) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/HtmlBlockParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HtmlBlockParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 12/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// `HtmlBlockParser` is a block parser which parses HTML blocks and returns them in form of 25 | /// `htmlBlock` cases of the `Block` enumeration. `HtmlBlockParser` does that with the help 26 | /// of `HtmlBlockParserPlugin` objects to which it delegates detecting of the various HTML 27 | /// block variants that are supported. 28 | /// 29 | open class HtmlBlockParser: BlockParser { 30 | 31 | /// List of supported HTML block parser plugin types; override this computed property in 32 | /// subclasses of `HtmlBlockParser` to create customized versions. 33 | open class var supportedParsers: [HtmlBlockParserPlugin.Type] { 34 | return [ScriptBlockParserPlugin.self, 35 | CommentBlockParserPlugin.self, 36 | ProcessingInstructionBlockParserPlugin.self, 37 | DeclarationBlockParserPlugin.self, 38 | CdataBlockParserPlugin.self, 39 | HtmlTagBlockParserPlugin.self 40 | ] 41 | } 42 | 43 | /// HTML block parser plugins 44 | private var htmlParsers: [HtmlBlockParserPlugin] 45 | 46 | /// Default initializer 47 | public required init(docParser: DocumentParser) { 48 | self.htmlParsers = [] 49 | for parserType in type(of: self).supportedParsers { 50 | self.htmlParsers.append(parserType.init()) 51 | } 52 | super.init(docParser: docParser) 53 | } 54 | 55 | open override func parse() -> ParseResult { 56 | guard self.shortLineIndent, self.line[self.contentStartIndex] == "<" else { 57 | return .none 58 | } 59 | var cline = self.line[self.contentStartIndex.. Bool { 96 | switch ch { 97 | case " ", "\t", "\n", "\r", "\r\n", "\u{b}", "\u{c}": 98 | return true 99 | default: 100 | return false 101 | } 102 | } 103 | 104 | open func line(_ line: String, 105 | at: String.Index, 106 | startsWith str: String, 107 | endsWith suffix: String? = nil, 108 | htmlTagSuffix: Bool = true) -> Bool { 109 | var strIndex: String.Index = str.startIndex 110 | var index = at 111 | while strIndex < str.endIndex { 112 | guard index < line.endIndex, line[index] == str[strIndex] else { 113 | return false 114 | } 115 | strIndex = str.index(after: strIndex) 116 | index = line.index(after: index) 117 | } 118 | if htmlTagSuffix { 119 | guard index < line.endIndex else { 120 | return true 121 | } 122 | switch line[index] { 123 | case " ", "\t", "\u{b}", "\u{c}": 124 | return true 125 | case "\n", "\r", "\r\n": 126 | return true 127 | case ">": 128 | return true 129 | default: 130 | if let end = suffix { 131 | strIndex = end.startIndex 132 | while strIndex < end.endIndex { 133 | guard index < line.endIndex, line[index] == end[strIndex] else { 134 | return false 135 | } 136 | strIndex = end.index(after: strIndex) 137 | index = line.index(after: index) 138 | } 139 | return true 140 | } 141 | return false 142 | } 143 | } else { 144 | return true 145 | } 146 | } 147 | 148 | open func startCondition(_ line: String) -> Bool { 149 | return false 150 | } 151 | 152 | open func endCondition(_ line: String) -> Bool { 153 | return false 154 | } 155 | 156 | open var emptyLineTerminator: Bool { 157 | return false 158 | } 159 | } 160 | 161 | public final class ScriptBlockParserPlugin: HtmlBlockParserPlugin { 162 | 163 | public override func startCondition(_ line: String) -> Bool { 164 | return self.line(line, at: line.startIndex, startsWith: " Bool { 170 | return line.contains("") || 171 | line.contains("") || 172 | line.contains("") 173 | } 174 | } 175 | 176 | public final class CommentBlockParserPlugin: HtmlBlockParserPlugin { 177 | 178 | public override func startCondition(_ line: String) -> Bool { 179 | return self.line(line, at: line.startIndex, startsWith: "") 184 | } 185 | } 186 | 187 | public final class ProcessingInstructionBlockParserPlugin: HtmlBlockParserPlugin { 188 | 189 | public override func startCondition(_ line: String) -> Bool { 190 | return self.line(line, at: line.startIndex, startsWith: " Bool { 194 | return line.contains("?>") 195 | } 196 | } 197 | 198 | public final class DeclarationBlockParserPlugin: HtmlBlockParserPlugin { 199 | 200 | public override func startCondition(_ line: String) -> Bool { 201 | var index: String.Index = line.startIndex 202 | guard index < line.endIndex && line[index] == "<" else { 203 | return false 204 | } 205 | index = line.index(after: index) 206 | guard index < line.endIndex && line[index] == "!" else { 207 | return false 208 | } 209 | index = line.index(after: index) 210 | guard index < line.endIndex else { 211 | return false 212 | } 213 | switch line[index] { 214 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", 215 | "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z": 216 | return true 217 | default: 218 | return false 219 | } 220 | } 221 | 222 | public override func endCondition(_ line: String) -> Bool { 223 | return line.contains(">") 224 | } 225 | } 226 | 227 | public final class CdataBlockParserPlugin: HtmlBlockParserPlugin { 228 | 229 | public override func startCondition(_ line: String) -> Bool { 230 | return self.line(line, at: line.startIndex, startsWith: " Bool { 234 | return line.contains("]]>") 235 | } 236 | } 237 | 238 | public final class HtmlTagBlockParserPlugin: HtmlBlockParserPlugin { 239 | final let htmlTags = ["address", "article", "aside", "base", "basefont", "blockquote", "body", 240 | "caption", "center", "col", "colgroup", "dd", "details", "dialog", "dir", 241 | "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", 242 | "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", 243 | "hr", "html", "iframe", "legend", "li", "link", "main", "menu", "menuitem", 244 | "nav", "noframes", "ol", "optgroup", "option", "p", "param", "section", 245 | "source", "summary", "table", "tbody", "td", "tfoot", "th", "thead", 246 | "title", "tr", "track", "ul"] 247 | 248 | public override func startCondition(_ line: String) -> Bool { 249 | var index = line.startIndex 250 | guard index < line.endIndex && line[index] == "<" else { 251 | return false 252 | } 253 | index = line.index(after: index) 254 | if index < line.endIndex && line[index] == "/" { 255 | index = line.index(after: index) 256 | } 257 | for htmlTag in self.htmlTags { 258 | if self.line(line, at: index, startsWith: htmlTag, endsWith: "/>") { 259 | return true 260 | } 261 | } 262 | return false 263 | } 264 | 265 | public override var emptyLineTerminator: Bool { 266 | return true 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/InlineParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 30/05/2019. 6 | // Copyright © 2019-2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// An `InlineParser` implements Markdown inline text markup parsing given a list of 25 | /// `InlineTransformer` classes as its configuration. `InlineParser` objects are not 26 | /// stateful and can be reused to parse the inline text of many Markdown blocks. 27 | /// 28 | open class InlineParser { 29 | 30 | /// Sequence of inline transformers which implement the inline parsing functionality. 31 | private var inlineTransformers: [InlineTransformer] 32 | 33 | /// Blocks of input document 34 | private let block: Block 35 | 36 | /// Link reference declarations 37 | public private(set) var linkRefDef: [String : (String, String?)] 38 | 39 | /// Initializer 40 | init(inlineTransformers: [InlineTransformer.Type], input: Block) { 41 | self.block = input 42 | self.linkRefDef = [:] 43 | self.inlineTransformers = [] 44 | for transformerType in inlineTransformers { 45 | self.inlineTransformers.append(transformerType.init(owner: self)) 46 | } 47 | } 48 | 49 | /// Traverses the input block and applies all inline transformers to all text. 50 | open func parse() -> Block { 51 | // First, collect all link reference definitions 52 | self.collectLinkRefDef(self.block) 53 | // Second, apply inline transformers 54 | return self.parse(self.block) 55 | } 56 | 57 | /// Traverses a Markdown block and enters link reference definitions into `linkRefDef`. 58 | public func collectLinkRefDef(_ block: Block) { 59 | switch block { 60 | case .document(let blocks): 61 | self.collectLinkRefDef(blocks) 62 | case .blockquote(let blocks): 63 | self.collectLinkRefDef(blocks) 64 | case .list(_, _, let blocks): 65 | self.collectLinkRefDef(blocks) 66 | case .listItem(_, _, let blocks): 67 | self.collectLinkRefDef(blocks) 68 | case .referenceDef(let label, let dest, let title): 69 | if title.isEmpty { 70 | let canonical = label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 71 | self.linkRefDef[canonical] = (String(dest), nil) 72 | } else { 73 | var str = "" 74 | for line in title { 75 | str += line.description 76 | } 77 | self.linkRefDef[label] = (String(dest), str) 78 | } 79 | default: 80 | break 81 | } 82 | } 83 | 84 | /// Traverses an array of Markdown blocks and enters link reference definitions 85 | /// into `linkRefDef`. 86 | public func collectLinkRefDef(_ blocks: Blocks) { 87 | for block in blocks { 88 | self.collectLinkRefDef(block) 89 | } 90 | } 91 | 92 | /// Parses a Markdown block and returns a new block in which all inline text markup 93 | /// is represented using `TextFragment` objects. 94 | open func parse(_ block: Block) -> Block { 95 | switch block { 96 | case .document(let blocks): 97 | return .document(self.parse(blocks)) 98 | case .blockquote(let blocks): 99 | return .blockquote(self.parse(blocks)) 100 | case .list(let start, let tight, let blocks): 101 | return .list(start, tight, self.parse(blocks)) 102 | case .listItem(let type, let tight, let blocks): 103 | return .listItem(type, tight, self.parse(blocks)) 104 | case .paragraph(let lines): 105 | return .paragraph(self.transform(lines)) 106 | case .thematicBreak: 107 | return .thematicBreak 108 | case .heading(let level, let lines): 109 | return .heading(level, self.transform(lines)) 110 | case .indentedCode(let lines): 111 | return .indentedCode(lines) 112 | case .fencedCode(let info, let lines): 113 | return .fencedCode(info, lines) 114 | case .htmlBlock(let lines): 115 | return .htmlBlock(lines) 116 | case .referenceDef(let label, let dest, let title): 117 | return .referenceDef(label, dest, title) 118 | case .table(let header, let align, let rows): 119 | return .table(self.transform(header), align, self.transform(rows)) 120 | case .definitionList(let defs): 121 | return .definitionList(self.transform(defs)) 122 | case .custom(let customBlock): 123 | return customBlock.parse(via: self) 124 | } 125 | } 126 | 127 | /// Parses a sequence of Markdown blocks and returns a new sequence in which all inline 128 | /// text markup is represented using `TextFragment` objects. 129 | public func parse(_ blocks: Blocks) -> Blocks { 130 | var res: Blocks = [] 131 | for block in blocks { 132 | res.append(self.parse(block)) 133 | } 134 | return res 135 | } 136 | 137 | /// Transforms raw Markdown text and returns a new `Text` object in which all inline markup 138 | /// is represented using `TextFragment` objects. 139 | public func transform(_ text: Text) -> Text { 140 | var res = text 141 | for transformer in self.inlineTransformers { 142 | res = transformer.transform(res) 143 | } 144 | return res 145 | } 146 | 147 | /// Transforms raw Markdown rows and returns a new `Row` object in which all inline markup 148 | /// is represented using `TextFragment` objects. 149 | public func transform(_ row: Row) -> Row { 150 | var res = Row() 151 | for cell in row { 152 | res.append(self.transform(cell)) 153 | } 154 | return res 155 | } 156 | 157 | /// Transforms raw Markdown tables and returns a new `Rows` object in which all inline markup 158 | /// is represented using `TextFragment` objects. 159 | public func transform(_ rows: Rows) -> Rows { 160 | var res = Rows() 161 | for row in rows { 162 | res.append(self.transform(row)) 163 | } 164 | return res 165 | } 166 | 167 | public func transform(_ defs: Definitions) -> Definitions { 168 | var res = Definitions() 169 | for def in defs { 170 | res.append(Definition(item: self.transform(def.item), 171 | descriptions: self.parse(def.descriptions))) 172 | } 173 | return res 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/InlineTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineTransformer.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 01/06/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// Class `InlineTransformer` defines a framework for plugins which transform a given 25 | /// unstructured or semi-structured text with Markdown markup into a structured 26 | /// representation which uses `TextFragment` objects. MarkdownKit implements a separate 27 | /// inline transformer plugin for every class of supported inline markup. 28 | /// 29 | open class InlineTransformer { 30 | 31 | public unowned let owner: InlineParser 32 | 33 | required public init(owner: InlineParser) { 34 | self.owner = owner 35 | } 36 | 37 | open func transform(_ text: Text) -> Text { 38 | var res: Text = Text() 39 | var iterator = text.makeIterator() 40 | var element = iterator.next() 41 | while let fragment = element { 42 | element = self.transform(fragment, from: &iterator, into: &res) 43 | } 44 | return res 45 | } 46 | 47 | open func transform(_ fragment: TextFragment, 48 | from iterator: inout Text.Iterator, 49 | into res: inout Text) -> TextFragment? { 50 | switch fragment { 51 | case .text(_): 52 | res.append(fragment: fragment) 53 | case .code(_): 54 | res.append(fragment: fragment) 55 | case .emph(let inner): 56 | res.append(fragment: .emph(self.transform(inner))) 57 | case .strong(let inner): 58 | res.append(fragment: .strong(self.transform(inner))) 59 | case .link(let inner, let uri, let title): 60 | res.append(fragment: .link(self.transform(inner), uri, title)) 61 | case .autolink(_, _): 62 | res.append(fragment: fragment) 63 | case .image(let inner, let uri, let title): 64 | res.append(fragment: .image(self.transform(inner), uri, title)) 65 | case .html(_): 66 | res.append(fragment: fragment) 67 | case .delimiter(_, _, _): 68 | res.append(fragment: fragment) 69 | case .softLineBreak, .hardLineBreak: 70 | res.append(fragment: fragment) 71 | case .custom(let customTextFragment): 72 | res.append(fragment: customTextFragment.transform(via: self)) 73 | } 74 | return iterator.next() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/LinkRefDefinitionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkRefDefinitionParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 11/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A block parser which parses link reference definitions returning `referenceDef` blocks. 25 | /// 26 | open class LinkRefDefinitionParser: RestorableBlockParser { 27 | 28 | public override var mayInterruptParagraph: Bool { 29 | return false 30 | } 31 | 32 | public override func parse() -> BlockParser.ParseResult { 33 | guard self.shortLineIndent && self.line[self.contentStartIndex] == "[" else { 34 | return .none 35 | } 36 | return super.parse() 37 | } 38 | 39 | public override func tryParse() -> ParseResult { 40 | var index = self.contentStartIndex 41 | guard let label = self.parseLabel(index: &index), 42 | index < self.contentEndIndex, 43 | self.line[index] == ":" else { 44 | return .none 45 | } 46 | index = self.line.index(after: index) 47 | guard self.skipSpace(index: &index) != nil else { 48 | return .none 49 | } 50 | let destination: Substring 51 | if self.line[index] == "<" { 52 | var prevBackslash = false 53 | index = self.line.index(after: index) 54 | let destStart = index 55 | while index < self.contentEndIndex && (prevBackslash || self.line[index] != ">") { 56 | guard prevBackslash || self.line[index] != "<" else { 57 | return .none 58 | } 59 | prevBackslash = !prevBackslash && self.line[index] == "\\" 60 | index = self.line.index(after: index) 61 | } 62 | guard index < self.contentEndIndex else { 63 | return .none 64 | } 65 | destination = self.line[destStart..= self.contentEndIndex || isWhitespace(self.line[index]) else { 81 | return .none 82 | } 83 | let onNewLine = self.skipSpace(index: &index) 84 | guard onNewLine != nil else { 85 | return .block(.referenceDef(label, destination, [])) 86 | } 87 | let title: Lines 88 | switch self.line[index] { 89 | case "\"": 90 | title = self.parseMultiLine(index: &index, closing: "\"", requireWhitespaces: true) 91 | case "'": 92 | title = self.parseMultiLine(index: &index, closing: "'", requireWhitespaces: true) 93 | case "(": 94 | title = self.parseMultiLine(index: &index, closing: ")", requireWhitespaces: true) 95 | default: 96 | guard index == self.contentStartIndex else { 97 | return .none 98 | } 99 | return .block(.referenceDef(label, destination, [])) 100 | } 101 | if title.isEmpty { 102 | if onNewLine == true { 103 | return .block(.referenceDef(label, destination, [])) 104 | } else { 105 | return .none 106 | } 107 | } 108 | skipWhitespace(in: self.line, from: &index, to: self.contentEndIndex) 109 | guard index >= self.contentEndIndex else { 110 | return .none 111 | } 112 | self.readNextLine() 113 | return .block(.referenceDef(label, destination, title)) 114 | } 115 | 116 | public static func balanced(_ str: Substring) -> Bool { 117 | var open = 0 118 | var index = str.startIndex 119 | var prevBackslash = false 120 | while index < str.endIndex { 121 | switch str[index] { 122 | case "(": 123 | if !prevBackslash { 124 | open += 1 125 | } 126 | case ")": 127 | if !prevBackslash { 128 | open -= 1 129 | guard open >= 0 else { 130 | return false 131 | } 132 | } 133 | case "\\": 134 | prevBackslash = !prevBackslash && str[index] == "\\" 135 | default: 136 | break 137 | } 138 | index = str.index(after: index) 139 | } 140 | return open == 0 141 | } 142 | 143 | private func skipSpace(index: inout Substring.Index) -> Bool? { 144 | var newline = false 145 | skipWhitespace(in: self.line, from: &index, to: self.contentEndIndex) 146 | if index >= self.contentEndIndex { 147 | self.readNextLine() 148 | newline = true 149 | if self.finished { 150 | return nil 151 | } 152 | index = self.contentStartIndex 153 | skipWhitespace(in: self.line, from: &index, to: self.contentEndIndex) 154 | if index >= self.contentEndIndex { 155 | return nil 156 | } 157 | } 158 | return newline 159 | } 160 | 161 | private func parseLabel(index: inout Substring.Index) -> String? { 162 | let labelLines = self.parseMultiLine(index: &index, closing: "]", requireWhitespaces: false) 163 | var res = "" 164 | for line in labelLines { 165 | let components = line.components(separatedBy: .whitespaces) 166 | let newLine = components.filter { !$0.isEmpty }.joined(separator: " ") 167 | if !newLine.isEmpty { 168 | if res.isEmpty { 169 | res = newLine 170 | } else { 171 | res.append(" ") 172 | res.append(newLine) 173 | } 174 | } 175 | } 176 | let length = res.count 177 | return length > 0 && length < 1000 ? res : nil 178 | } 179 | 180 | private func parseMultiLine(index: inout Substring.Index, 181 | closing closeCh: Character, 182 | requireWhitespaces: Bool) -> Lines { 183 | let openCh = self.line[index] 184 | index = self.line.index(after: index) 185 | var start = index 186 | var prevBackslash = false 187 | var res: Lines = [] 188 | while !self.finished && !self.lineEmpty { 189 | while index < self.contentEndIndex && 190 | (prevBackslash || self.line[index] != closeCh) { 191 | if !prevBackslash && self.line[index] == openCh { 192 | return [] 193 | } 194 | prevBackslash = !prevBackslash && self.line[index] == "\\" 195 | index = self.line.index(after: index) 196 | } 197 | if index >= self.contentEndIndex { 198 | res.append(self.line[start..= self.contentEndIndex else { 211 | return [] 212 | } 213 | } 214 | return res 215 | } 216 | } 217 | return [] 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/LinkTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkTransformer.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 24/06/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// An inline transformer which extracts link and image link markup and transforms it into 25 | /// `link` and `image` text fragments. 26 | /// 27 | open class LinkTransformer: InlineTransformer { 28 | 29 | public override func transform(_ text: Text) -> Text { 30 | var res = Text() 31 | var iterator = text.makeIterator() 32 | var element = iterator.next() 33 | loop: while let fragment = element { 34 | if case .delimiter("[", _, let type) = fragment { 35 | var scanner = iterator 36 | var next = scanner.next() 37 | var open = 0 38 | var inner = Text() 39 | scan: while let lookahead = next { 40 | switch lookahead { 41 | case .delimiter("]", _, _): 42 | if open == 0 { 43 | if let link = self.complete(link: type.isEmpty, inner, with: &scanner) { 44 | res.append(fragment: link) 45 | iterator = scanner 46 | element = iterator.next() 47 | continue loop 48 | } 49 | break scan 50 | } 51 | open -= 1 52 | case .delimiter("[", _, _): 53 | open += 1 54 | default: 55 | break 56 | } 57 | // if type.isEmpty { 58 | inner.append(fragment: lookahead) 59 | next = scanner.next() 60 | // } else { 61 | // next = self.transform(lookahead, from: &scanner, into: &inner) 62 | // } 63 | } 64 | res.append(fragment: fragment) 65 | element = iterator.next() 66 | } else { 67 | element = self.transform(fragment, from: &iterator, into: &res) 68 | } 69 | } 70 | return res 71 | } 72 | 73 | private func complete(link: Bool, 74 | _ text: Text, 75 | with iterator: inout Text.Iterator) -> TextFragment? { 76 | let initial = iterator 77 | let next = iterator.next() 78 | guard let element = next else { 79 | return nil 80 | } 81 | switch element { 82 | case .delimiter("(", _, _): 83 | if let res = self.completeInline(link: link, text, with: &iterator) { 84 | return res 85 | } 86 | case .delimiter("[", _, _): 87 | if let res = self.completeRef(link: link, text, with: &iterator) { 88 | return res 89 | } 90 | default: 91 | break 92 | } 93 | let components = text.description.components(separatedBy: .whitespacesAndNewlines) 94 | let label = components.filter { !$0.isEmpty }.joined(separator: " ").lowercased() 95 | if label.count < 1000, 96 | let (uri, title) = self.owner.linkRefDef[label] { 97 | let text = self.transform(text) 98 | if link && self.containsLink(text) { 99 | return nil 100 | } 101 | iterator = initial 102 | return link ? .link(text, uri, title) : .image(text, uri, title) 103 | } else { 104 | return nil 105 | } 106 | } 107 | 108 | private func completeInline(link: Bool, 109 | _ text: Text, 110 | with iterator: inout Text.Iterator) -> TextFragment? { 111 | // Skip whitespace 112 | var element = self.skipWhitespace(for: &iterator) 113 | guard let dest = element else { 114 | return nil 115 | } 116 | // Transform link description 117 | let text = self.transform(text) 118 | if link && self.containsLink(text) { 119 | return nil 120 | } 121 | // Parse destination 122 | var destination = "" 123 | choose: switch dest { 124 | // Is this a link destination surrounded by `<` and `>` 125 | case .delimiter("<", _, _): 126 | element = iterator.next() 127 | loop: while let fragment = element { 128 | switch fragment { 129 | case .delimiter(">", _, _): 130 | break loop 131 | case .delimiter("<", _, _): 132 | return nil 133 | case .hardLineBreak, .softLineBreak: 134 | return nil 135 | default: 136 | destination += fragment.rawDescription 137 | } 138 | element = iterator.next() 139 | } 140 | case .html(let str): 141 | if str.contains("\n") { 142 | return nil 143 | } 144 | destination += str 145 | case .autolink(_, let str): 146 | if str.contains("\n") { 147 | return nil 148 | } 149 | destination += str 150 | // Parsing regular destinations 151 | default: 152 | var open = 0 153 | if case .some(.text(let str)) = element, 154 | let index = str.firstIndex(where: { ch in !isAsciiWhitespaceOrControl(ch) }), 155 | index < str.endIndex { 156 | destination += str[index.. String? { 227 | var element = iterator.next() 228 | var title = "" 229 | while let fragment = element { 230 | switch fragment { 231 | case .delimiter(ch, _, _): 232 | return title 233 | default: 234 | title += fragment.description 235 | } 236 | element = iterator.next() 237 | } 238 | return nil 239 | } 240 | 241 | private func skipWhitespace(for iterator: inout Text.Iterator) -> TextFragment? { 242 | var element = iterator.next() 243 | while let fragment = element { 244 | switch fragment { 245 | case .hardLineBreak, .softLineBreak: 246 | break 247 | case .text(let str) where isWhitespaceString(str): 248 | break 249 | default: 250 | return element 251 | } 252 | element = iterator.next() 253 | } 254 | return nil 255 | } 256 | 257 | private func containsLink(_ text: Text) -> Bool { 258 | for fragment in text { 259 | switch fragment { 260 | case .emph(let inner): 261 | if self.containsLink(inner) { 262 | return true 263 | } 264 | case .strong(let inner): 265 | if self.containsLink(inner) { 266 | return true 267 | } 268 | case .link(_, _, _): 269 | return true 270 | case .autolink(_, _): 271 | return true 272 | case .image(let inner, _, _): 273 | if self.containsLink(inner) { 274 | return true 275 | } 276 | default: 277 | break 278 | } 279 | } 280 | return false 281 | } 282 | 283 | private func completeRef(link: Bool, 284 | _ text: Text, 285 | with iterator: inout Text.Iterator) -> TextFragment? { 286 | // Skip whitespace 287 | var element = self.skipWhitespace(for: &iterator) 288 | // Transform link description 289 | let text = self.transform(text) 290 | if link && self.containsLink(text) { 291 | return nil 292 | } 293 | // Parse label 294 | var label = "" 295 | while let fragment = element { 296 | switch fragment { 297 | case .delimiter("]", _, _): 298 | label = label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 299 | if let (uri, title) = self.owner.linkRefDef[label] { 300 | return link ? .link(text, uri, title) : .image(text, uri, title) 301 | } else { 302 | return nil 303 | } 304 | case .softLineBreak, .hardLineBreak: 305 | label.append(" ") 306 | default: 307 | let components = fragment.description.components(separatedBy: .whitespaces) 308 | label.append(components.filter { !$0.isEmpty }.joined(separator: " ")) 309 | if label.count > 999 { 310 | return nil 311 | } 312 | } 313 | element = iterator.next() 314 | } 315 | return nil 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/ListItemParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListItemParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 05/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A block parser for parsing list items. There are two types of list items: 25 | /// _bullet list items_ and _ordered list items_. They are represented using `listItem` blocks 26 | /// using either the `bullet` or the `ordered list type. 27 | /// 28 | open class ListItemParser: BlockParser { 29 | 30 | /// Set of supported bullet characters. 31 | private let bulletChars: Set 32 | 33 | /// Used for extending `ListItemParser` 34 | public init(docParser: DocumentParser, bulletChars: Set) { 35 | self.bulletChars = bulletChars 36 | super.init(docParser: docParser) 37 | } 38 | 39 | public required init(docParser: DocumentParser) { 40 | self.bulletChars = ["-", "+", "*"] 41 | super.init(docParser: docParser) 42 | } 43 | 44 | private class BulletListItemContainer: NestedContainer { 45 | let bullet: Character 46 | let indent: Int 47 | 48 | init(bullet: Character, tight: Bool, indent: Int, outer: Container) { 49 | self.bullet = bullet 50 | self.indent = indent 51 | super.init(outer: outer) 52 | self.density = .init(tight: tight) 53 | } 54 | 55 | public override func skipIndent(input: String, 56 | startIndex: String.Index, 57 | endIndex: String.Index) -> String.Index? { 58 | var index = startIndex 59 | var indent = 0 60 | loop: while index < endIndex && indent < self.indent { 61 | switch input[index] { 62 | case " ": 63 | indent += 1 64 | case "\t": 65 | indent += 4 66 | default: 67 | break loop 68 | } 69 | index = input.index(after: index) 70 | } 71 | guard index <= endIndex && indent >= self.indent else { 72 | return nil 73 | } 74 | return index 75 | } 76 | 77 | public override func makeBlock(_ docParser: DocumentParser) -> Block { 78 | return .listItem(.bullet(self.bullet), self.density ?? .tight, docParser.bundle(blocks: self.content)) 79 | } 80 | 81 | public override var debugDescription: String { 82 | return self.outer.debugDescription + " <- bulletListItem(\(self.bullet))" 83 | } 84 | } 85 | 86 | private final class OrderedListItemContainer: BulletListItemContainer { 87 | let number: Int 88 | 89 | init(number: Int, delimiter: Character, tight: Bool, indent: Int, outer: Container) { 90 | self.number = number 91 | super.init(bullet: delimiter, tight: tight, indent: indent, outer: outer) 92 | } 93 | 94 | public override func makeBlock(_ docParser: DocumentParser) -> Block { 95 | return .listItem(.ordered(self.number, self.bullet), 96 | self.density ?? .tight, 97 | docParser.bundle(blocks: self.content)) 98 | } 99 | 100 | public override var debugDescription: String { 101 | return self.outer.debugDescription + " <- orderedListItem(\(self.number), \(self.bullet))" 102 | } 103 | } 104 | 105 | public override func parse() -> ParseResult { 106 | guard self.shortLineIndent else { 107 | return .none 108 | } 109 | var i = self.contentStartIndex 110 | var listMarkerIndent = 0 111 | var marker: Character = self.line[i] 112 | var number: Int? = nil 113 | switch marker { 114 | case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": 115 | var n = self.line[i].wholeNumberValue! 116 | i = self.line.index(after: i) 117 | listMarkerIndent += 1 118 | numloop: while i < self.contentEndIndex && listMarkerIndent < 8 { 119 | switch self.line[i] { 120 | case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": 121 | n = n * 10 + self.line[i].wholeNumberValue! 122 | default: 123 | break numloop 124 | } 125 | i = self.line.index(after: i) 126 | listMarkerIndent += 1 127 | } 128 | guard i < self.contentEndIndex else { 129 | return .none 130 | } 131 | number = n 132 | marker = self.line[i] 133 | switch marker { 134 | case ".", ")": 135 | break 136 | default: 137 | return .none 138 | } 139 | default: 140 | if self.bulletChars.contains(marker) { 141 | break 142 | } 143 | return .none 144 | } 145 | i = self.line.index(after: i) 146 | listMarkerIndent += 1 147 | var indent = 0 148 | loop: while i < self.contentEndIndex && indent < 4 { 149 | switch self.line[i] { 150 | case " ": 151 | indent += 1 152 | case "\t": 153 | indent += 4 154 | default: 155 | break loop 156 | } 157 | i = self.line.index(after: i) 158 | } 159 | guard i >= self.contentEndIndex || indent > 0 else { 160 | return .none 161 | } 162 | if indent > 4 { 163 | indent = 1 164 | } 165 | indent += self.lineIndent + listMarkerIndent 166 | self.docParser.resetLineStart(i) 167 | let tight = !self.prevLineEmpty 168 | if let number = number { 169 | return .container { encl in 170 | OrderedListItemContainer(number: number, 171 | delimiter: marker, 172 | tight: tight, 173 | indent: indent, 174 | outer: encl) 175 | } 176 | } else { 177 | return .container { encl in 178 | BulletListItemContainer(bullet: marker, tight: tight, indent: indent, outer: encl) 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/MarkdownParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 03/05/2019. 6 | // Copyright © 2019-2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// `MarkdownParser` objects are used to parse Markdown text represented as a string. 25 | /// The `MarkdownParser` object itself defines the configuration of the parser. It is 26 | /// stateless in the sense that it can be used for parsing many input strings. This is 27 | /// done via the `parse` function. `parse` returns an abstract syntac tree representing 28 | /// the Markdown text for the given input string. 29 | /// 30 | /// The `parse` method of the `MarkdownParser` object delegates parsing of the input 31 | /// string to two types of processors: a `BlockParser` object and an `InlineTransformer` 32 | /// object. A `BlockParser` parses the Markdown block structure returning an abstract 33 | /// syntax tree ignoring inline markup. An `InlineTransformer` object is used to parse 34 | /// a particular type of inline markup within text of Markdown blocks, replacing the 35 | /// matching text with an abstract syntax tree representing the markup. 36 | /// 37 | /// The `parse` method of `MarkdownParser` operates in two phases: in the first phase, 38 | /// the block structure of an input string is identified via the `BlockParser`s. In the 39 | /// second phase, the block structure gets traversed and markup within raw text gets 40 | /// replaced with a structured representation. 41 | /// 42 | open class MarkdownParser { 43 | 44 | /// The default list of block parsers. The order of this list matters. 45 | open class var defaultBlockParsers: [BlockParser.Type] { 46 | return self.blockParsers 47 | } 48 | 49 | private static let blockParsers: [BlockParser.Type] = MarkdownParser.headingParsers + [ 50 | IndentedCodeBlockParser.self, 51 | FencedCodeBlockParser.self, 52 | HtmlBlockParser.self, 53 | LinkRefDefinitionParser.self, 54 | BlockquoteParser.self, 55 | ListItemParser.self 56 | ] 57 | 58 | public static let headingParsers: [BlockParser.Type] = [ 59 | AtxHeadingParser.self, 60 | SetextHeadingParser.self, 61 | ThematicBreakParser.self 62 | ] 63 | 64 | /// The default list of inline transformers. The order of this list matters. 65 | open class var defaultInlineTransformers: [InlineTransformer.Type] { 66 | return self.inlineTransformers 67 | } 68 | 69 | private static let inlineTransformers: [InlineTransformer.Type] = [ 70 | DelimiterTransformer.self, 71 | CodeLinkHtmlTransformer.self, 72 | LinkTransformer.self, 73 | EmphasisTransformer.self, 74 | EscapeTransformer.self 75 | ] 76 | 77 | /// Defines a default implementation 78 | open class var standard: MarkdownParser { 79 | return self.singleton 80 | } 81 | 82 | private static let singleton: MarkdownParser = MarkdownParser() 83 | 84 | /// A custom list of block parsers; if this is provided via the constructor, it overrides 85 | /// the `defaultBlockParsers`. 86 | private let customBlockParsers: [BlockParser.Type]? 87 | 88 | /// A custom list of inline transformers; if this is provided via the constructor, it overrides 89 | /// the `defaultInlineTransformers`. 90 | private let customInlineTransformers: [InlineTransformer.Type]? 91 | 92 | /// Block parsing gets delegated to a stateful `DocumentParser` object which implements a 93 | /// protocol for invoking the `BlockParser` objects that its initializer is creating based 94 | /// on the types provided in the `blockParsers` parameter. 95 | public func documentParser(input: String) -> DocumentParser { 96 | return self.documentParser(blockParsers: self.customBlockParsers ?? 97 | type(of: self).defaultBlockParsers, 98 | input: input) 99 | } 100 | 101 | /// Factory method to customize document parsing in subclasses. 102 | open func documentParser(blockParsers: [BlockParser.Type], input: String) -> DocumentParser { 103 | return DocumentParser(blockParsers: blockParsers, input: input) 104 | } 105 | 106 | /// Inline parsing is performed via a stateless `InlineParser` object which implements a 107 | /// protocol for invoking the `InlineTransformer` objects. Since the inline parser is stateless, 108 | /// a single object gets created lazily and reused for parsing all input. 109 | public func inlineParser(input: Block) -> InlineParser { 110 | return self.inlineParser(inlineTransformers: self.customInlineTransformers ?? 111 | type(of: self).defaultInlineTransformers, 112 | input: input) 113 | } 114 | 115 | /// Factory method to customize inline parsing in subclasses. 116 | open func inlineParser(inlineTransformers: [InlineTransformer.Type], 117 | input: Block) -> InlineParser { 118 | return InlineParser(inlineTransformers: inlineTransformers, input: input) 119 | } 120 | 121 | /// Constructor of `MarkdownParser` objects; it takes a list of block parsers, a list of 122 | /// inline transformers as well as an input string as its parameters. 123 | public init(blockParsers: [BlockParser.Type]? = nil, 124 | inlineTransformers: [InlineTransformer.Type]? = nil) { 125 | self.customBlockParsers = blockParsers 126 | self.customInlineTransformers = inlineTransformers 127 | } 128 | 129 | /// Invokes the parser and returns an abstract syntx tree of the Markdown syntax. 130 | /// If `blockOnly` is set to `true` (default is `false`), only the block parsers are 131 | /// invoked and no inline parsing gets performed. 132 | public func parse(_ str: String, blockOnly: Bool = false) -> Block { 133 | let doc = self.documentParser(input: str).parse() 134 | if blockOnly { 135 | return doc 136 | } else { 137 | return self.inlineParser(input: doc).parse() 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/ParserUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParserUtil.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 10/06/2019. 6 | // Copyright © 2019-2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | 24 | public func isAsciiWhitespaceOrControl(_ ch: Character) -> Bool { 25 | return isWhitespace(ch) || isControlCharacter(ch) 26 | } 27 | 28 | public func isWhitespace(_ ch: Character) -> Bool { 29 | switch ch { 30 | case " ", "\t", "\n", "\r", "\u{b}", "\u{c}": 31 | return true 32 | default: 33 | return false 34 | } 35 | } 36 | 37 | public func isWhitespaceString(_ str: Substring) -> Bool { 38 | for ch in str { 39 | if !isWhitespace(ch) { 40 | return false 41 | } 42 | } 43 | return true 44 | } 45 | 46 | public func isUnicodeWhitespace(_ ch: Character) -> Bool { 47 | if let scalar = ch.unicodeScalars.first { 48 | return CharacterSet.whitespacesAndNewlines.contains(scalar) 49 | } 50 | return false 51 | } 52 | 53 | public func isSpace(_ ch: Character) -> Bool { 54 | return ch == " " 55 | } 56 | 57 | public func isDash(_ ch: Character) -> Bool { 58 | return ch == "-" 59 | } 60 | 61 | public func isAsciiPunctuation(_ ch: Character) -> Bool { 62 | switch ch { 63 | case "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":", 64 | ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~": 65 | return true 66 | default: 67 | return false 68 | } 69 | } 70 | 71 | public func isUppercaseAsciiLetter(_ ch: Character) -> Bool { 72 | switch ch { 73 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", 74 | "R", "S", "T", "U", "V", "W", "X", "Y", "Z": 75 | return true 76 | default: 77 | return false 78 | } 79 | } 80 | 81 | public func isAsciiLetter(_ ch: Character) -> Bool { 82 | switch ch { 83 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", 84 | "R", "S", "T", "U", "V", "W", "X", "Y", "Z", 85 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", 86 | "r", "s", "t", "u", "v", "w", "x", "y", "z": 87 | return true 88 | default: 89 | return false 90 | } 91 | } 92 | 93 | public func isAsciiLetterOrDigit(_ ch: Character) -> Bool { 94 | switch ch { 95 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", 96 | "R", "S", "T", "U", "V", "W", "X", "Y", "Z", 97 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", 98 | "r", "s", "t", "u", "v", "w", "x", "y", "z", 99 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": 100 | return true 101 | default: 102 | return false 103 | } 104 | } 105 | 106 | public func isControlCharacter(_ ch: Character) -> Bool { 107 | if let scalar = ch.unicodeScalars.first, CharacterSet.controlCharacters.contains(scalar) { 108 | return true 109 | } 110 | return false 111 | } 112 | 113 | public func isUnicodePunctuation(_ ch: Character) -> Bool { 114 | if let scalar = ch.unicodeScalars.first, CharacterSet.punctuationCharacters.contains(scalar) { 115 | return true 116 | } 117 | return isAsciiPunctuation(ch) 118 | } 119 | 120 | public func skipWhitespace(in str: Substring, 121 | from index: inout Substring.Index, 122 | to endIndex: Substring.Index) { 123 | while index < endIndex { 124 | let ch = str[index] 125 | if ch != " " { 126 | guard let scalar = ch.unicodeScalars.first, CharacterSet.whitespaces.contains(scalar) else { 127 | return 128 | } 129 | } 130 | index = str.index(after: index) 131 | } 132 | } 133 | 134 | public func isURI(_ str: String) -> Bool { 135 | var iterator = str.makeIterator() 136 | var next = iterator.next() 137 | guard next != nil, next!.isASCII, next!.isLetter else { 138 | return false 139 | } 140 | next = iterator.next() 141 | var n = 1 142 | while let ch = next { 143 | guard ch.isASCII else { 144 | return false 145 | } 146 | if ch == ":" { 147 | if n > 1 { 148 | break 149 | } else { 150 | return false 151 | } 152 | } 153 | guard ch.isLetter || ch.isHexDigit || ch == "+" || ch == "-" || ch == "." else { 154 | return false 155 | } 156 | next = iterator.next() 157 | n += 1 158 | guard n <= 32 else { 159 | return false 160 | } 161 | } 162 | guard next != nil else { 163 | return false 164 | } 165 | while let ch = iterator.next() { 166 | guard !isWhitespace(ch), !isControlCharacter(ch), ch != "<", ch != ">" else { 167 | return false 168 | } 169 | } 170 | return true 171 | } 172 | 173 | public func isHtmlTag(_ str: String) -> Bool { 174 | var iterator = str.makeIterator() 175 | var next = iterator.next() 176 | guard let ch = next else { 177 | return false 178 | } 179 | switch ch { 180 | case "/": 181 | next = iterator.next() 182 | guard skipTagName(&next, &iterator) else { 183 | return false 184 | } 185 | _ = skipWhitespace(&next, &iterator) 186 | return next == nil 187 | case "?": 188 | return str.count > 1 && str.last! == "?" 189 | case "!": 190 | guard let fst = iterator.next() else { 191 | return false 192 | } 193 | if fst == "-" { 194 | guard let snd = iterator.next(), snd == "-" else { 195 | return false 196 | } 197 | guard str.count > 4, 198 | !str.hasPrefix("!-->"), 199 | !str.hasPrefix("!--->"), 200 | !str.hasSuffix("---") else { 201 | return false 202 | } 203 | return !str[str.index(str.startIndex, offsetBy: 3).." { 230 | break loop 231 | } 232 | skipped = skipAttribute(&next, &iterator) 233 | } 234 | guard skipped! else { 235 | return false 236 | } 237 | } 238 | if case .some("/") = next { 239 | next = iterator.next() 240 | } 241 | return next == nil 242 | } 243 | } 244 | 245 | fileprivate func skipAttribute(_ next: inout Character?, 246 | _ iterator: inout String.Iterator) -> Bool? { 247 | guard skipAttributeName(&next, &iterator) else { 248 | return false 249 | } 250 | _ = skipWhitespace(&next, &iterator) 251 | guard case .some("=") = next else { 252 | return nil 253 | } 254 | next = iterator.next() 255 | _ = skipWhitespace(&next, &iterator) 256 | guard let fst = next else { 257 | return false 258 | } 259 | next = iterator.next() 260 | switch fst { 261 | case "'": 262 | while let ch = next, ch != "'" { 263 | next = iterator.next() 264 | } 265 | guard next != nil else { 266 | return false 267 | } 268 | next = iterator.next() 269 | case "\"": 270 | while let ch = next, ch != "\"" { 271 | next = iterator.next() 272 | } 273 | guard next != nil else { 274 | return false 275 | } 276 | next = iterator.next() 277 | default: 278 | while let ch = next, !isWhitespace(ch), 279 | ch != "\"", ch != "'", ch != "=", ch != "<", ch != ">", ch != "`" { 280 | next = iterator.next() 281 | } 282 | } 283 | return true 284 | } 285 | 286 | fileprivate func skipAttributeName(_ next: inout Character?, 287 | _ iterator: inout String.Iterator) -> Bool { 288 | guard let fst = next, isAsciiLetter(fst) || fst == "_" || fst == ":" else { 289 | return false 290 | } 291 | next = iterator.next() 292 | while let ch = next { 293 | guard isAsciiLetterOrDigit(ch) || ch == "_" || ch == "-" || ch == "." || ch == ":" else { 294 | return true 295 | } 296 | next = iterator.next() 297 | } 298 | return true 299 | } 300 | 301 | fileprivate func skipTagName(_ next: inout Character?, 302 | _ iterator: inout String.Iterator) -> Bool { 303 | guard let fst = next, isAsciiLetter(fst) else { 304 | return false 305 | } 306 | next = iterator.next() 307 | while let ch = next { 308 | guard isAsciiLetterOrDigit(ch) || ch == "-" else { 309 | return true 310 | } 311 | next = iterator.next() 312 | } 313 | return true 314 | } 315 | 316 | fileprivate func skipWhitespace(_ next: inout Character?, 317 | _ iterator: inout String.Iterator) -> Bool { 318 | guard let fst = next, isWhitespace(fst) else { 319 | return false 320 | } 321 | next = iterator.next() 322 | while let ch = next { 323 | guard isWhitespace(ch) else { 324 | return true 325 | } 326 | next = iterator.next() 327 | } 328 | return true 329 | } 330 | 331 | // Detect email addresses 332 | 333 | fileprivate let emailRegExpr: NSRegularExpression = 334 | try! NSRegularExpression(pattern: #"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]"# 335 | + #"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]"# 336 | + #"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"#) 337 | 338 | public func isEmailAddress(_ str: String) -> Bool { 339 | return emailRegExpr.firstMatch(in: str, 340 | range: NSRange(location: 0, length: str.utf16.count)) != nil 341 | } 342 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/SetextHeadingParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetextHeadingParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 12/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A block parser which parses setext headings (headers with text underlining) returning 25 | /// `heading` blocks. 26 | /// 27 | open class SetextHeadingParser: BlockParser { 28 | 29 | public override func parse() -> ParseResult { 30 | guard self.shortLineIndent, 31 | !self.lazyContinuation, 32 | let plines = self.prevParagraphLines, 33 | !plines.isEmpty else { 34 | return .none 35 | } 36 | let ch = self.line[self.contentStartIndex] 37 | let level: Int 38 | switch ch { 39 | case "=": 40 | level = 1 41 | case "-": 42 | level = 2 43 | default: 44 | return .none 45 | } 46 | var i = self.contentStartIndex 47 | while i < self.contentEndIndex && self.line[i] == ch { 48 | i = self.line.index(after: i) 49 | } 50 | skipWhitespace(in: self.line, from: &i, to: self.contentEndIndex) 51 | guard i >= self.contentEndIndex else { 52 | return .none 53 | } 54 | self.consumeParagraphLines() 55 | self.readNextLine() 56 | return .block(.heading(level, plines.finalized())) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Parser/TableParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableParser.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 17/07/2020. 6 | // Copyright © 2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// A block parser which parses tables returning `table` blocks. 25 | /// 26 | open class TableParser: RestorableBlockParser { 27 | 28 | open override func parse() -> ParseResult { 29 | guard self.shortLineIndent else { 30 | return .none 31 | } 32 | var i = self.contentStartIndex 33 | var prev = Character(" ") 34 | while i < self.contentEndIndex { 35 | if self.line[i] == "|" && prev != "\\" { 36 | return super.parse() 37 | } 38 | prev = self.line[i] 39 | i = self.line.index(after: i) 40 | } 41 | return .none 42 | } 43 | 44 | open override func tryParse() -> ParseResult { 45 | guard let header = self.parseRow() else { 46 | return .none 47 | } 48 | self.readNextLine() 49 | guard let alignrow = self.parseRow(), alignrow.count == header.count else { 50 | return .none 51 | } 52 | var alignments = Alignments() 53 | for cell in alignrow { 54 | guard case .some(.text(let str)) = cell.first, str.count > 0 else { 55 | return .none 56 | } 57 | var check: Substring 58 | if str.first! == ":" { 59 | if str.count > 2 && str.last! == ":" { 60 | alignments.append(.center) 61 | check = str[str.index(after: str.startIndex).. header.count { 83 | row.removeLast(row.count - header.count) 84 | // Append cells if parsed row has too few 85 | } else if row.count < header.count { 86 | for _ in row.count.. Row? { 97 | var i = self.contentStartIndex 98 | skipWhitespace(in: self.line, from: &i, to: self.contentEndIndex) 99 | guard i < self.contentEndIndex else { 100 | return nil 101 | } 102 | var validRow = false 103 | if self.line[i] == "|" { 104 | validRow = true 105 | i = self.line.index(after: i) 106 | skipWhitespace(in: self.line, from: &i, to: self.contentEndIndex) 107 | } 108 | var res = Row() 109 | var text: Text? = nil 110 | while i < self.contentEndIndex { 111 | var j = i 112 | var k = i 113 | var prev = Character(" ") 114 | while j < self.contentEndIndex && (self.line[j] != "|" || prev == "\\") { 115 | prev = self.line[j] 116 | j = self.line.index(after: j) 117 | if prev != " " { 118 | k = j 119 | } 120 | } 121 | if j < self.contentEndIndex { 122 | if text == nil { 123 | res.append(Text(self.line[i.. ParseResult { 29 | guard self.shortLineIndent else { 30 | return .none 31 | } 32 | var i = self.contentStartIndex 33 | let ch = self.line[i] 34 | switch ch { 35 | case "-", "_", "*": 36 | break 37 | default: 38 | return .none 39 | } 40 | var count = 0 41 | while i < self.contentEndIndex { 42 | switch self.line[i] { 43 | case " ", "\t": 44 | break 45 | case ch: 46 | count += 1 47 | default: 48 | return .none 49 | } 50 | i = self.line.index(after: i) 51 | } 52 | guard count >= 3 else { 53 | return .none 54 | } 55 | self.readNextLine() 56 | return .block(.thematicBreak) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/Text.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Text.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 30/05/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// Struct `Text` is used to represent inline text. A `Text` struct consists of a sequence 25 | /// of `TextFragment` objects. 26 | /// 27 | public struct Text: Collection, Equatable, CustomStringConvertible, CustomDebugStringConvertible { 28 | public typealias Index = ContiguousArray.Index 29 | public typealias Iterator = ContiguousArray.Iterator 30 | 31 | private var fragments: ContiguousArray = [] 32 | 33 | public init(_ str: Substring? = nil) { 34 | if let str = str { 35 | self.fragments.append(.text(str)) 36 | } 37 | } 38 | 39 | public init(_ fragment: TextFragment) { 40 | self.fragments.append(fragment) 41 | } 42 | 43 | /// Returns `true` if the text is empty. 44 | public var isEmpty: Bool { 45 | return self.fragments.isEmpty 46 | } 47 | 48 | /// Returns the first text fragment if available. 49 | public var first: TextFragment? { 50 | return self.fragments.first 51 | } 52 | 53 | /// Returns the last text fragment if available. 54 | public var last: TextFragment? { 55 | return self.fragments.last 56 | } 57 | 58 | /// Appends a line of text, potentially followed by a hard line break 59 | mutating public func append(line: Substring, withHardLineBreak: Bool) { 60 | let n = self.fragments.count 61 | if n > 0, case .text(let str) = self.fragments[n - 1] { 62 | if str.last == "\\" { 63 | let newline = str[str.startIndex.. Iterator { 93 | return self.fragments.makeIterator() 94 | } 95 | 96 | /// Returns the start index. 97 | public var startIndex: Index { 98 | return self.fragments.startIndex 99 | } 100 | 101 | /// Returns the end index. 102 | public var endIndex: Index { 103 | return self.fragments.endIndex 104 | } 105 | 106 | /// Returns the text fragment at the given index. 107 | public subscript (position: Index) -> Iterator.Element { 108 | return self.fragments[position] 109 | } 110 | 111 | /// Advances the given index by one place. 112 | public func index(after i: Index) -> Index { 113 | return self.fragments.index(after: i) 114 | } 115 | 116 | /// Returns a description of this `Text` object as a string as if the text would be 117 | /// represented in Markdown. 118 | public var description: String { 119 | var res = "" 120 | for fragment in self.fragments { 121 | res = res + fragment.description 122 | } 123 | return res 124 | } 125 | 126 | /// Returns a raw description of this `Text` object as a string, i.e. as if the text 127 | /// would be represented in Markdown but ignoring all markup. 128 | public var rawDescription: String { 129 | return self.fragments.map { $0.rawDescription }.joined() 130 | } 131 | 132 | /// Returns a raw description of this `Text` object as a string for which all markup 133 | /// gets ignored. 134 | public var string: String { 135 | return self.fragments.map { $0.string }.joined() 136 | } 137 | 138 | /// Returns a debug description of this `Text` object. 139 | public var debugDescription: String { 140 | var res = "" 141 | for fragment in self.fragments { 142 | if res.isEmpty { 143 | res = fragment.debugDescription 144 | } else { 145 | res = res + ", \(fragment.debugDescription)" 146 | } 147 | } 148 | return res 149 | } 150 | 151 | /// Finalizes the `Text` object by removing trailing line breaks. 152 | public func finalized() -> Text { 153 | if let lastLine = self.fragments.last { 154 | switch lastLine { 155 | case .hardLineBreak, .softLineBreak: 156 | var plines = self 157 | plines.fragments.removeLast() 158 | return plines 159 | default: 160 | return self 161 | } 162 | } else { 163 | return self 164 | } 165 | } 166 | 167 | /// Defines an equality relationship for `Text` objects. 168 | public static func == (lhs: Text, rhs: Text) -> Bool { 169 | return lhs.fragments == rhs.fragments 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/MarkdownKit/TextFragment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFragment.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 14/07/2019. 6 | // Copyright © 2019-2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | 23 | /// 24 | /// In MarkdownKit, text with markup is represented as a sequence of `TextFragment` objects. 25 | /// Each `TextFragment` enumeration variant represents one form of inline markup. Since 26 | /// markup can be arbitrarily nested, this is a recursive data structure (via struct `Text`). 27 | /// 28 | public enum TextFragment: Equatable, CustomStringConvertible, CustomDebugStringConvertible { 29 | case text(Substring) 30 | case code(Substring) 31 | case emph(Text) 32 | case strong(Text) 33 | case link(Text, String?, String?) 34 | case autolink(AutolinkType, Substring) 35 | case image(Text, String?, String?) 36 | case html(Substring) 37 | case delimiter(Character, Int, DelimiterRunType) 38 | case softLineBreak 39 | case hardLineBreak 40 | case custom(CustomTextFragment) 41 | 42 | /// Returns a description of this `TextFragment` object as a string as if the text would be 43 | /// represented in Markdown. 44 | public var description: String { 45 | switch self { 46 | case .text(let str): 47 | return str.description 48 | case .code(let str): 49 | return "`\(str.description)`" 50 | case .emph(let text): 51 | return "*\(text.description)*" 52 | case .strong(let text): 53 | return "**\(text.description)**" 54 | case .link(let text, let uri, let title): 55 | return "[\(text.description)](\(uri?.description ?? "") \(title?.description ?? ""))" 56 | case .autolink(_, let uri): 57 | return "<\(uri.description)>" 58 | case .image(let text, let uri, let title): 59 | return "![\(text.description)](\(uri?.description ?? "") \(title?.description ?? ""))" 60 | case .html(let tag): 61 | return "<\(tag.description)>" 62 | case .delimiter(let ch, let n, let type): 63 | var res = String(ch) 64 | for _ in 1.." 97 | case .delimiter(let ch, let n, let type): 98 | var res = String(ch) 99 | for _ in 1.. Bool { 163 | switch (lhs, rhs) { 164 | case (.text(let llstr), .text(let rstr)): 165 | return llstr == rstr 166 | case (.code(let lstr), .code(let rstr)): 167 | return lstr == rstr 168 | case (.emph(let ltext), .emph(let rtext)): 169 | return ltext == rtext 170 | case (.strong(let ltext), .strong(let rtext)): 171 | return ltext == rtext 172 | case (.link(let ltext, let ls1, let ls2), .link(let rtext, let rs1, let rs2)): 173 | return ltext == rtext && ls1 == rs1 && ls2 == rs2 174 | case (.autolink(let ltype, let lstr), .autolink(let rtype, let rstr)): 175 | return ltype == rtype && lstr == rstr 176 | case (.image(let ltext, let ls1, let ls2), .image(let rtext, let rs1, let rs2)): 177 | return ltext == rtext && ls1 == rs1 && ls2 == rs2 178 | case (.html(let lstr), .html(let rstr)): 179 | return lstr == rstr 180 | case (.delimiter(let lch, let ln, let ld), .delimiter(let rch, let rn, let rd)): 181 | return lch == rch && ln == rn && ld == rd 182 | case (.softLineBreak, .softLineBreak): 183 | return true 184 | case (.hardLineBreak, .hardLineBreak): 185 | return true 186 | case (.custom(let lctf), .custom(let rctf)): 187 | return lctf.equals(to: rctf) 188 | default: 189 | return false 190 | } 191 | } 192 | } 193 | 194 | /// 195 | /// Represents an autolink type. 196 | /// 197 | public enum AutolinkType: Equatable, CustomStringConvertible, CustomDebugStringConvertible { 198 | case uri 199 | case email 200 | 201 | public var description: String { 202 | switch self { 203 | case .uri: 204 | return "uri" 205 | case .email: 206 | return "email" 207 | } 208 | } 209 | 210 | public var debugDescription: String { 211 | return self.description 212 | } 213 | } 214 | 215 | /// 216 | /// Lines are arrays of substrings. 217 | /// 218 | public typealias Lines = ContiguousArray 219 | 220 | /// 221 | /// Each delimiter run is classified into a set of types which are represented via the 222 | /// `DelimiterRunType` struct. 223 | public struct DelimiterRunType: OptionSet, CustomStringConvertible { 224 | public let rawValue: UInt8 225 | 226 | public init(rawValue: UInt8) { 227 | self.rawValue = rawValue 228 | } 229 | 230 | public static let leftFlanking = DelimiterRunType(rawValue: 1 << 0) 231 | public static let rightFlanking = DelimiterRunType(rawValue: 1 << 1) 232 | public static let leftPunctuation = DelimiterRunType(rawValue: 1 << 2) 233 | public static let rightPunctuation = DelimiterRunType(rawValue: 1 << 3) 234 | public static let escaped = DelimiterRunType(rawValue: 1 << 4) 235 | public static let image = DelimiterRunType(rawValue: 1 << 5) 236 | 237 | public var description: String { 238 | var res = "" 239 | if self.rawValue & 0x1 == 0x1 { 240 | res = "\(res)\(res.isEmpty ? "" : ", ")leftFlanking" 241 | } 242 | if self.rawValue & 0x2 == 0x2 { 243 | res = "\(res)\(res.isEmpty ? "" : ", ")rightFlanking" 244 | } 245 | if self.rawValue & 0x4 == 0x4 { 246 | res = "\(res)\(res.isEmpty ? "" : ", ")leftPunctuation" 247 | } 248 | if self.rawValue & 0x8 == 0x8 { 249 | res = "\(res)\(res.isEmpty ? "" : ", ")rightPunctuation" 250 | } 251 | if self.rawValue & 0x10 == 0x10 { 252 | res = "\(res)\(res.isEmpty ? "" : ", ")escaped" 253 | } 254 | if self.rawValue & 0x20 == 0x20 { 255 | res = "\(res)\(res.isEmpty ? "" : ", ")image" 256 | } 257 | return "[\(res)]" 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Sources/MarkdownKitProcess/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // MarkdownKitProcess 4 | // 5 | // Created by Matthias Zenger on 01/08/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | import MarkdownKit 23 | 24 | // This is a command-line tool for converting a text file in Markdown format into 25 | // HTML. The tool also allows converting a whole folder of Markdown files into HTML. 26 | 27 | let fileManager = FileManager.default 28 | 29 | // Utility functions 30 | 31 | func markdownFiles(inDir baseUrl: URL) -> [URL] { 32 | var res: [URL] = [] 33 | if let urls = try? fileManager.contentsOfDirectory( 34 | at: baseUrl, 35 | includingPropertiesForKeys: [.nameKey, .isDirectoryKey], 36 | options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) { 37 | for url in urls { 38 | let values = try? url.resourceValues(forKeys: [.isDirectoryKey]) 39 | if !(values?.isDirectory ?? false) && url.lastPathComponent.hasSuffix(".md") { 40 | res.append(url) 41 | } 42 | } 43 | } 44 | return res 45 | } 46 | 47 | func baseUrl(for path: String, role: String) -> (URL, Bool) { 48 | var isDir = ObjCBool(false) 49 | guard fileManager.fileExists(atPath: path, isDirectory: &isDir) else { 50 | print("\(role) '\(path)' does not exist") 51 | exit(1) 52 | } 53 | return (URL(fileURLWithPath: path, isDirectory: isDir.boolValue), isDir.boolValue) 54 | } 55 | 56 | // Command-line argument handling 57 | 58 | guard CommandLine.arguments.count > 1 && CommandLine.arguments.count < 4 else { 59 | print("usage: mdkitprocess []") 60 | print("where: is either a Markdown file or a directory containing Markdown files") 61 | print(" is either an HTML file or a directory in which HTML files are written") 62 | exit(0) 63 | } 64 | 65 | var sourceTarget: [(URL, URL?)] = [] 66 | 67 | let (sourceBaseUrl, sourceIsDir) = baseUrl(for: CommandLine.arguments[1], role: "source") 68 | if CommandLine.arguments.count == 2 { 69 | let sources = sourceIsDir ? markdownFiles(inDir: sourceBaseUrl) : [sourceBaseUrl] 70 | for source in sources { 71 | let target = source.deletingPathExtension().appendingPathExtension("html") 72 | sourceTarget.append((source, target)) 73 | } 74 | } else if CommandLine.arguments[2] == "-" { 75 | guard !sourceIsDir else { 76 | print("cannot print source directory to console") 77 | exit(1) 78 | } 79 | sourceTarget.append((sourceBaseUrl, nil)) 80 | } else { 81 | let (targetBaseUrl, targetIsDir) = baseUrl(for: CommandLine.arguments[2], role: "target") 82 | guard sourceIsDir == targetIsDir else { 83 | print("source and target either need to be directories or individual files") 84 | exit(1) 85 | } 86 | if sourceIsDir { 87 | let sources = markdownFiles(inDir: sourceBaseUrl) 88 | for source in sources { 89 | let target = targetBaseUrl.appendingPathComponent(source.lastPathComponent) 90 | .deletingPathExtension() 91 | .appendingPathExtension("html") 92 | sourceTarget.append((source, target)) 93 | } 94 | } else { 95 | sourceTarget.append((sourceBaseUrl, targetBaseUrl)) 96 | } 97 | } 98 | 99 | // Processing 100 | 101 | for (sourceUrl, optTargetUrl) in sourceTarget { 102 | if let textContent = try? String(contentsOf: sourceUrl) { 103 | let markdownContent = MarkdownParser.standard.parse(textContent) 104 | let htmlContent = HtmlGenerator.standard.generate(doc: markdownContent) 105 | if let targetUrl = optTargetUrl { 106 | if fileManager.fileExists(atPath: targetUrl.path) { 107 | print("cannot overwrite target file '\(targetUrl.path)'") 108 | } else { 109 | do { 110 | try htmlContent.write(to: targetUrl, atomically: false, encoding: .utf8) 111 | print("converted '\(sourceUrl.lastPathComponent)' into '\(targetUrl.lastPathComponent)'") 112 | } catch { 113 | print("cannot write target file '\(targetUrl.path)'") 114 | } 115 | } 116 | } else { 117 | print(htmlContent) 118 | } 119 | } else { 120 | print("cannot read source file '\(sourceUrl.path)'") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinuxMain.swift 3 | // MarkdownKit 4 | // 5 | // Created by Matthias Zenger on 14/02/2021. 6 | // Copyright © 2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | #if os(Linux) 22 | 23 | import XCTest 24 | @testable import MarkdownKitTests 25 | 26 | XCTMain( 27 | [ 28 | testCase(MarkdownBlockTests.allTests), 29 | testCase(ExtendedMarkdownBlockTests.allTests), 30 | testCase(MarkdownInlineTests.allTests), 31 | testCase(MarkdownHtmlTests.allTests), 32 | testCase(ExtendedMarkdownHtmlTests.allTests), 33 | testCase(MarkdownStringTests.allTests), 34 | ] 35 | ) 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /Tests/MarkdownKitTests/ExtendedMarkdownHtmlTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtendedMarkdownHtmlTests.swift 3 | // MarkdownKitTests 4 | // 5 | // Created by Matthias Zenger on 18/07/2020. 6 | // Copyright © 2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import XCTest 22 | @testable import MarkdownKit 23 | 24 | class ExtendedMarkdownHtmlTests: XCTestCase, MarkdownKitFactory { 25 | 26 | private func generateHtml(_ str: String) -> String { 27 | return HtmlGenerator().generate(doc: ExtendedMarkdownParser.standard.parse(str)) 28 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 29 | } 30 | 31 | func testSimpleNestedLists() { 32 | XCTAssertEqual( 33 | generateHtml("- Apple\n\t- Banana"), 34 | "
      \n
    • Apple\n
        \n
      • Banana
      • \n
      \n
    • \n
    ") 35 | } 36 | 37 | func testTables() { 38 | XCTAssertEqual(generateHtml(" Column A | Column B\n" + 39 | " -------- | --------\n"), 40 | "\n" + 41 | "\n" + 42 | "\n" + 43 | "
    Column AColumn B
    ") 44 | XCTAssertEqual(generateHtml(" Column A | Column B\n" + 45 | " -------- | --------\n" + 46 | " 1 | 2 \n"), 47 | "\n" + 48 | "\n" + 49 | "\n" + 50 | "\n" + 51 | "
    Column AColumn B
    12
    ") 52 | XCTAssertEqual(generateHtml(" Column A |**Column B**\n" + 53 | " :------- | :------:\n" + 54 | " 1 | 2 \n" + 55 | " reg *it* | __bold__\n"), 56 | "\n" + 57 | "" + 58 | "\n" + 59 | "\n" + 60 | "\n" + 61 | "" + 62 | "\n" + 63 | "
    Column AColumn B
    12
    reg itbold
    ") 64 | } 65 | 66 | func testDescriptionLists() { 67 | XCTAssertEqual(generateHtml("Item **name**\n" + 68 | ": Description of\n" + 69 | " _item_"), 70 | "
    \n" + 71 | "
    Item name
    \n" + 72 | "
    Description of\nitem
    \n" + 73 | "
    ") 74 | XCTAssertEqual(generateHtml("Item name\n" + 75 | ": Description of\n" + 76 | "item\n" + 77 | ": Another description\n\n" + 78 | "Item two\n" + 79 | ": Description two\n" + 80 | ": Description three\n"), 81 | "
    \n" + 82 | "
    Item name
    \n" + 83 | "
    Description of\nitem
    \n" + 84 | "
    Another description
    \n" + 85 | "
    Item two
    \n" + 86 | "
    Description two
    \n" + 87 | "
    Description three
    \n" + 88 | "
    ") 89 | } 90 | 91 | static let allTests = [ 92 | ("testSimpleNestedLists", testSimpleNestedLists), 93 | ("testTables", testTables), 94 | ("testDescriptionLists", testDescriptionLists), 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /Tests/MarkdownKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.1.3 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/MarkdownKitTests/MarkdownASTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownASTests.swift 3 | // MarkdownKitTests 4 | // 5 | // Created by Matthias Zenger on 26/02/2022. 6 | // Copyright © 2022 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import XCTest 22 | import MarkdownKit 23 | 24 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 25 | 26 | class MarkdownASTests: XCTestCase { 27 | 28 | private func generateHtml(imageBaseUrl: URL? = nil, _ str: String) -> String { 29 | return AttributedStringGenerator(imageBaseUrl: imageBaseUrl) 30 | .htmlGenerator 31 | .generate(doc: MarkdownParser.standard.parse(str)) 32 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 33 | } 34 | 35 | func testSimpleNestedLists() { 36 | XCTAssertEqual( 37 | generateHtml("- Apple\n\t- Banana"), 38 | "
      \n
    • Apple\n
        \n
      • Banana
      • \n
      \n
    • \n
    \n

    ") 39 | XCTAssertEqual( 40 | AttributedStringGenerator(options: [.tightLists]) 41 | .htmlGenerator 42 | .generate(doc: MarkdownParser.standard.parse("- Apple\n\t- Banana")) 43 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines), 44 | "

      \n
    • Apple\n
        \n
      • Banana
      • \n
      \n
    • \n
    \n

    ") 45 | } 46 | 47 | func testRelativeImageUrls() { 48 | XCTAssertEqual(generateHtml("![Test image](imagefile.png)"), 49 | "

    \"Test

    ") 50 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"), 51 | "![Test image](imagefile.png)"), 52 | "

    \"Test

    ") 53 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"), 54 | "![Test image](/imagefile.png)"), 55 | "

    \"Test

    ") 56 | XCTAssertEqual(generateHtml("![Test image](one/imagefile.png)"), 57 | "

    \"Test

    ") 58 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"), 59 | "![Test image](one/imagefile.png)"), 60 | "

    \"Test

    ") 61 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"), 62 | "![Test image](/one/imagefile.png)"), 63 | "

    \"Test

    ") 64 | } 65 | } 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /Tests/MarkdownKitTests/MarkdownExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownExtension.swift 3 | // MarkdownKitTests 4 | // 5 | // Created by Matthias Zenger on 11/05/2021. 6 | // Copyright © 2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import XCTest 22 | @testable import MarkdownKit 23 | 24 | class MarkdownExtension: XCTestCase, MarkdownKitFactory { 25 | 26 | enum LineEmphasis: CustomTextFragment { 27 | case underline(Text) 28 | case strikethrough(Text) 29 | 30 | func equals(to other: CustomTextFragment) -> Bool { 31 | guard let that = other as? LineEmphasis else { 32 | return false 33 | } 34 | switch (self, that) { 35 | case (.underline(let lhs), .underline(let rhs)): 36 | return lhs == rhs 37 | case (.strikethrough(let lhs), .strikethrough(let rhs)): 38 | return lhs == rhs 39 | default: 40 | return false 41 | } 42 | } 43 | 44 | func transform(via transformer: InlineTransformer) -> TextFragment { 45 | switch self { 46 | case .underline(let text): 47 | return .custom(LineEmphasis.underline(transformer.transform(text))) 48 | case .strikethrough(let text): 49 | return .custom(LineEmphasis.strikethrough(transformer.transform(text))) 50 | } 51 | } 52 | 53 | func generateHtml(via htmlGen: HtmlGenerator) -> String { 54 | switch self { 55 | case .underline(let text): 56 | return "" + htmlGen.generate(text: text) + "" 57 | case .strikethrough(let text): 58 | return "" + htmlGen.generate(text: text) + "" 59 | } 60 | } 61 | 62 | func generateHtml(via htmlGen: HtmlGenerator, 63 | and attrGen: AttributedStringGenerator?) -> String { 64 | return self.generateHtml(via: htmlGen) 65 | } 66 | 67 | var rawDescription: String { 68 | switch self { 69 | case .underline(let text): 70 | return text.rawDescription 71 | case .strikethrough(let text): 72 | return text.rawDescription 73 | } 74 | } 75 | 76 | var description: String { 77 | switch self { 78 | case .underline(let text): 79 | return "~\(text.description)~" 80 | case .strikethrough(let text): 81 | return "~~\(text.description)~~" 82 | } 83 | } 84 | 85 | var debugDescription: String { 86 | switch self { 87 | case .underline(let text): 88 | return "underline(\(text.debugDescription))" 89 | case .strikethrough(let text): 90 | return "strikethrough(\(text.debugDescription))" 91 | } 92 | } 93 | } 94 | 95 | final class EmphasisTestTransformer: EmphasisTransformer { 96 | override public class var supportedEmphasis: [Emphasis] { 97 | return super.supportedEmphasis + [ 98 | Emphasis(ch: "~", special: false, factory: { double, text in 99 | return .custom(double ? LineEmphasis.strikethrough(text) 100 | : LineEmphasis.underline(text)) 101 | })] 102 | } 103 | } 104 | 105 | final class DelimiterTestTransformer: DelimiterTransformer { 106 | override public class var emphasisChars: [Character] { 107 | return super.emphasisChars + ["~"] 108 | } 109 | } 110 | 111 | final class EmphasisTestMarkdownParser: MarkdownParser { 112 | override public class var defaultInlineTransformers: [InlineTransformer.Type] { 113 | return [DelimiterTestTransformer.self, 114 | CodeLinkHtmlTransformer.self, 115 | LinkTransformer.self, 116 | EmphasisTestTransformer.self, 117 | EscapeTransformer.self] 118 | } 119 | override public class var standard: EmphasisTestMarkdownParser { 120 | return self.singleton 121 | } 122 | private static let singleton: EmphasisTestMarkdownParser = EmphasisTestMarkdownParser() 123 | } 124 | 125 | private func parse(_ str: String) -> Block { 126 | return EmphasisTestMarkdownParser.standard.parse(str) 127 | } 128 | 129 | private func generateHtml(_ str: String) -> String { 130 | return HtmlGenerator().generate(doc: EmphasisTestMarkdownParser.standard.parse(str)) 131 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 132 | } 133 | 134 | func testExtendedDelimiters() throws { 135 | XCTAssertEqual(parse("~foo bar"), 136 | document(paragraph(.delimiter("~", 1, .leftFlanking), 137 | .text("foo bar")))) 138 | XCTAssertEqual(parse("~~foo bar"), 139 | document(paragraph(.delimiter("~", 2, .leftFlanking), 140 | .text("foo bar")))) 141 | XCTAssertEqual(parse("~~foo~ bar"), 142 | document(paragraph(.delimiter("~", 1, .leftFlanking), 143 | custom(LineEmphasis.underline, .text("foo")), 144 | .text(" bar")))) 145 | XCTAssertEqual(parse("~~foo\\~ bar"), 146 | document(paragraph(.delimiter("~", 2, .leftFlanking), 147 | .text("foo~ bar")))) 148 | XCTAssertEqual(parse("~~foo~~ bar"), 149 | document(paragraph(custom(LineEmphasis.strikethrough, .text("foo")), 150 | .text(" bar")))) 151 | XCTAssertEqual(parse("ok ~~~foo~~~ bar"), 152 | document(paragraph(.text("ok "), 153 | custom(LineEmphasis.underline, 154 | custom(LineEmphasis.strikethrough, .text("foo"))), 155 | .text(" bar")))) 156 | XCTAssertEqual(parse("combined *~foo~* bar"), 157 | document(paragraph(.text("combined "), 158 | emph(custom(LineEmphasis.underline, .text("foo"))), 159 | .text(" bar")))) 160 | XCTAssertEqual(parse("combined ~*foo bar*~"), 161 | document(paragraph(.text("combined "), 162 | custom(LineEmphasis.underline, emph(.text("foo bar")))))) 163 | XCTAssertEqual(parse("combined *~foo~ bar*"), 164 | document(paragraph(.text("combined "), 165 | emph(custom(LineEmphasis.underline, .text("foo")), 166 | .text(" bar"))))) 167 | } 168 | 169 | func testExtendedDelimitersHtml() { 170 | XCTAssertEqual(generateHtml("one ~two~\n~~three~~ four"), 171 | "

    one two\nthree four

    ") 172 | XCTAssertEqual(generateHtml("### Sub ~and~ heading ###\nAnd this is the text."), 173 | "

    Sub and heading

    \n

    And this is the text.

    ") 174 | XCTAssertEqual(generateHtml("expressive, ~~simple~, ~~and~~ ~elegant~~"), 175 | "

    expressive, simple, and elegant

    ") 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Tests/MarkdownKitTests/MarkdownFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownFactory.swift 3 | // MarkdownKitTests 4 | // 5 | // Created by Matthias Zenger on 09/05/2019. 6 | // Copyright © 2019-2020 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import Foundation 22 | @testable import MarkdownKit 23 | 24 | protocol MarkdownKitFactory { 25 | } 26 | 27 | extension MarkdownKitFactory { 28 | 29 | func document(_ bs: Block...) -> Block { 30 | return .document(ContiguousArray(bs)) 31 | } 32 | 33 | func paragraph(_ strs: String?...) -> Block { 34 | var res = Text() 35 | for str in strs { 36 | if let str = str { 37 | if let last = res.last { 38 | switch last { 39 | case .softLineBreak, .hardLineBreak: 40 | break 41 | default: 42 | res.append(fragment: .softLineBreak) 43 | } 44 | } 45 | 46 | res.append(fragment: .text(Substring(str))) 47 | } else { 48 | res.append(fragment: .hardLineBreak) 49 | } 50 | } 51 | return .paragraph(res) 52 | } 53 | 54 | func paragraph(_ fragments: TextFragment...) -> Block { 55 | var res = Text() 56 | for fragment in fragments { 57 | res.append(fragment: fragment) 58 | } 59 | return .paragraph(res) 60 | } 61 | 62 | func emph(_ fragments: TextFragment...) -> TextFragment { 63 | var res = Text() 64 | for fragment in fragments { 65 | res.append(fragment: fragment) 66 | } 67 | return .emph(res) 68 | } 69 | 70 | func strong(_ fragments: TextFragment...) -> TextFragment { 71 | var res = Text() 72 | for fragment in fragments { 73 | res.append(fragment: fragment) 74 | } 75 | return .strong(res) 76 | } 77 | 78 | func link(_ dest: String?, _ title: String?, _ fragments: TextFragment...) -> TextFragment { 79 | var res = Text() 80 | for fragment in fragments { 81 | res.append(fragment: fragment) 82 | } 83 | return .link(res, dest, title) 84 | } 85 | 86 | func image(_ dest: String?, _ title: String?, _ fragments: TextFragment...) -> TextFragment { 87 | var res = Text() 88 | for fragment in fragments { 89 | res.append(fragment: fragment) 90 | } 91 | return .image(res, dest, title) 92 | } 93 | 94 | func custom(_ factory: (Text) -> CustomTextFragment, 95 | _ fragments: TextFragment...) -> TextFragment { 96 | var res = Text() 97 | for fragment in fragments { 98 | res.append(fragment: fragment) 99 | } 100 | return .custom(factory(res)) 101 | } 102 | 103 | func atxHeading(_ level: Int, _ title: String) -> Block { 104 | return .heading(level, Text(Substring(title))) 105 | } 106 | 107 | func setextHeading(_ level: Int, _ strs: String?...) -> Block { 108 | var res = Text() 109 | for str in strs { 110 | if let str = str { 111 | if let last = res.last { 112 | switch last { 113 | case .softLineBreak, .hardLineBreak: 114 | break 115 | default: 116 | res.append(fragment: .softLineBreak) 117 | } 118 | } 119 | res.append(fragment: .text(Substring(str))) 120 | } else { 121 | res.append(fragment: .hardLineBreak) 122 | } 123 | } 124 | return .heading(level, res) 125 | } 126 | 127 | func blockquote(_ bs: Block...) -> Block { 128 | return .blockquote(ContiguousArray(bs)) 129 | } 130 | 131 | func indentedCode(_ strs: Substring...) -> Block { 132 | return .indentedCode(ContiguousArray(strs)) 133 | } 134 | 135 | func fencedCode(_ info: String?, _ strs: Substring...) -> Block { 136 | return .fencedCode(info, ContiguousArray(strs)) 137 | } 138 | 139 | func list(_ num: Int, tight: Bool = true, _ bs: Block...) -> Block { 140 | return .list(num, tight, ContiguousArray(bs)) 141 | } 142 | 143 | func list(tight: Bool = true, _ bs: Block...) -> Block { 144 | return .list(nil, tight, ContiguousArray(bs)) 145 | } 146 | 147 | func listItem(_ num: Int, 148 | _ sep: Character, 149 | tight: Bool = false, 150 | initial: Bool = false, 151 | _ bs: Block...) -> Block { 152 | return .listItem(.ordered(num, sep), 153 | tight ? .tight : (initial ? .initial : .loose), 154 | ContiguousArray(bs)) 155 | } 156 | 157 | func listItem(_ bullet: Character, tight: Bool = false, initial: Bool = false, _ bs: Block...) -> Block { 158 | return .listItem(.bullet(bullet), 159 | tight ? .tight : (initial ? .initial : .loose), 160 | ContiguousArray(bs)) 161 | } 162 | 163 | func htmlBlock(_ lines: Substring...) -> Block { 164 | return .htmlBlock(ContiguousArray(lines)) 165 | } 166 | 167 | func referenceDef(_ label: String, _ dest: Substring, _ title: Substring...) -> Block { 168 | return .referenceDef(label, dest, ContiguousArray(title)) 169 | } 170 | 171 | func table(_ hdr: [Substring?], _ algn: [Alignment], _ rw: [Substring?]...) -> Block { 172 | func toRow(_ arr: [Substring?]) -> Row { 173 | var res = Row() 174 | for a in arr { 175 | if let str = a { 176 | let components = str.components(separatedBy: "$") 177 | if components.count <= 1 { 178 | res.append(Text(str)) 179 | } else { 180 | var text = Text() 181 | for component in components { 182 | text.append(fragment: .text(Substring(component))) 183 | } 184 | res.append(text) 185 | } 186 | } else { 187 | res.append(Text()) 188 | } 189 | } 190 | return res 191 | } 192 | var rows = Rows() 193 | for r in rw { 194 | rows.append(toRow(r)) 195 | } 196 | return .table(toRow(hdr), ContiguousArray(algn), rows) 197 | } 198 | 199 | func definitionList(_ decls: (Substring, [(ListDensity, [Block])])...) -> Block { 200 | var defs = Definitions() 201 | for decl in decls { 202 | var res = Blocks() 203 | for (density, blocks) in decl.1 { 204 | var content = Blocks() 205 | for block in blocks { 206 | content.append(block) 207 | } 208 | res.append(.listItem(.bullet(":"), density, content)) 209 | } 210 | defs.append(Definition(item: Text(decl.0), descriptions: res)) 211 | } 212 | return .definitionList(defs) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Tests/MarkdownKitTests/MarkdownHtmlTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownHtmlTests.swift 3 | // MarkdownKitTests 4 | // 5 | // Created by Matthias Zenger on 20/07/2019. 6 | // Copyright © 2019 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import XCTest 22 | @testable import MarkdownKit 23 | 24 | class MarkdownHtmlTests: XCTestCase, MarkdownKitFactory { 25 | 26 | private func generateHtml(_ str: String) -> String { 27 | return HtmlGenerator().generate(doc: MarkdownParser.standard.parse(str)) 28 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 29 | } 30 | 31 | func testBasics() { 32 | XCTAssertEqual(generateHtml("one *two*\n**three** four"), 33 | "

    one two\nthree four

    ") 34 | XCTAssertEqual(generateHtml("one _two_ __three__\n***\nfour"), 35 | "

    one two three

    \n
    \n

    four

    ") 36 | XCTAssertEqual(generateHtml("# Top\n## Below\nAnd this is the text."), 37 | "

    Top

    \n

    Below

    \n

    And this is the text.

    ") 38 | XCTAssertEqual(generateHtml("### Sub *and* heading ###\nAnd this is the text."), 39 | "

    Sub and heading

    \n

    And this is the text.

    ") 40 | XCTAssertEqual(generateHtml("expressive & simple & elegant"), 41 | "

    expressive & simple & elegant

    ") 42 | XCTAssertEqual(generateHtml("This is `a & b`"), 43 | "

    This is a &amp; b

    ") 44 | } 45 | 46 | func testLists() { 47 | XCTAssertEqual(generateHtml(""" 48 | - One 49 | - Two 50 | - Three 51 | """), 52 | "
      \n
    • One
    • \n
    • Two
    • \n
    • Three
    • \n
    ") 53 | XCTAssertEqual(generateHtml(""" 54 | - One 55 | 56 | Two 57 | - Three 58 | - Four 59 | """), 60 | "
      \n
    • One

      \n

      Two

      \n
    • \n
    • Three

      \n
    • \n
    • " + 61 | "

      Four

      \n
    • \n
    ") 62 | } 63 | 64 | func testSimpleNestedLists() { 65 | XCTAssertEqual( 66 | generateHtml("- Apple\n\t- Banana"), 67 | "
      \n
    • Apple\n
        \n
      • Banana
      • \n
      \n
    • \n
    ") 68 | } 69 | 70 | func testNestedLists() { 71 | XCTAssertEqual(generateHtml(""" 72 | - foo 73 | - bar 74 | * one 75 | * two 76 | * three 77 | - goo 78 | """), 79 | "
      \n
    • foo
    • \n
    • bar\n
        \n
      • one
      • \n
      • two
      • \n" + 80 | "
      • three
      • \n
      \n
    • \n
    • goo
    • \n
    ") 81 | } 82 | 83 | func testImageLinks() { 84 | XCTAssertEqual(generateHtml(""" 85 | This is an inline image: ![example *image*](folder/image.jpg "image title"). 86 | """), 87 | "

    This is an inline image: \"example.

    ") 89 | XCTAssertEqual(generateHtml(""" 90 | This is an image block: 91 | 92 | ![example *image*](folder/image.jpg) 93 | """), 94 | "

    This is an image block:

    \n" + 95 | "

    \"example

    ") 96 | } 97 | 98 | func testAutolinks() { 99 | XCTAssertEqual(generateHtml("Test test"), 100 | "

    Test <www.example.com> test

    ") 101 | XCTAssertEqual(generateHtml("Test test"), 102 | "

    Test http://www.example.com test

    ") 103 | } 104 | 105 | func testCodeBlocks() { 106 | XCTAssertEqual(generateHtml("Test\n\n```\nThis should not be bold.\n```\n"), 107 | "

    Test

    \n
    This should <b>not be bold</b>.\n" +
    108 |                    "
    ") 109 | } 110 | 111 | static let allTests = [ 112 | ("testBasics", testBasics), 113 | ("testLists", testLists), 114 | ("testNestedLists", testNestedLists), 115 | ("testImageLinks", testImageLinks), 116 | ("testAutolinks", testAutolinks), 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /Tests/MarkdownKitTests/MarkdownStringTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkdownStringTests.swift 3 | // MarkdownKitTests 4 | // 5 | // Created by Matthias Zenger on 14/02/2021. 6 | // Copyright © 2021 Google LLC. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | import XCTest 22 | @testable import MarkdownKit 23 | 24 | class MarkdownStringTests: XCTestCase { 25 | 26 | func testAmpersandEncoding() throws { 27 | XCTAssertEqual("head tail".encodingPredefinedXmlEntities(), 28 | "head tail") 29 | XCTAssertEqual("head & tail".encodingPredefinedXmlEntities(), 30 | "head & tail") 31 | XCTAssertEqual("head && tail".encodingPredefinedXmlEntities(), 32 | "head && tail") 33 | } 34 | 35 | func testPredefinedEncodings() throws { 36 | XCTAssertEqual("head \"tail\"".encodingPredefinedXmlEntities(), 37 | "head "tail"") 38 | XCTAssertEqual("head'n tail".encodingPredefinedXmlEntities(), 39 | "head'n tail") 40 | XCTAssertEqual("\"'x\" corresponds to (quote x)".encodingPredefinedXmlEntities(), 41 | ""'x" corresponds to (quote x)") 42 | } 43 | 44 | func testDecodingEntities() throws { 45 | XCTAssertEqual(""'x" corresponds to (quote x)".decodingNamedCharacters(), 46 | "\"'x\" corresponds to (quote x)") 47 | XCTAssertEqual("head&tail is not "⋔"".decodingNamedCharacters(), 48 | "head&tail\u{000A0}is not \"⋔\"") 49 | XCTAssertEqual("x≉3.141".decodingNamedCharacters(), 50 | "x≉3.141") 51 | XCTAssertEqual("⋪⋬⋫".decodingNamedCharacters(), 52 | "⋪⋬⋫") 53 | } 54 | 55 | static let allTests = [ 56 | ("testAmpersandEncoding", testAmpersandEncoding), 57 | ("testPredefinedEncodings", testPredefinedEncodings), 58 | ("testDecodingEntities", testDecodingEntities), 59 | ] 60 | } 61 | --------------------------------------------------------------------------------