Compare commits

...

12 Commits

Author SHA1 Message Date
Sven e754ab1c80 Provide access to the list delimiter (#275)
* Provide access to the list delimiter

* Fix test
2021-10-18 13:22:18 +02:00
Sergey Pimenov 3b07bb52cf [Fix] DownTextView: Trigger re-render on styler change (#271)
Co-authored-by: John Nguyen <polyxo@protonmail.com>
2021-08-21 09:27:29 +02:00
John Nguyen 055f818ed7 Add codecov threshold (#272) 2021-08-21 09:07:28 +02:00
Oliver Fox 9997dc00cb RTL support in lists (#261)
- Added text alignment option to lists

Co-authored-by: John Nguyen <polyxo@protonmail.com>
2021-08-21 08:13:29 +02:00
Erik Kerber e2b597d469 [Chore] Add arm64 to simulator supported archs (#265) 2021-07-28 18:06:09 +02:00
John Nguyen bf24fcbb2d [Changes] v0.11.0 2021-05-04 21:56:02 +02:00
John Nguyen f34b166be1 [Pod] Bump to v0.11.0 2021-05-04 21:40:58 +02:00
Loïc Dardant 0586cec03b [Feature] Custom list prefixes for AttributedStringVisitor (#255)
* [Feature] Custom list prefixes for AttributedStringVisitor

* [Feature] Custom list prefixes for AttributedStringVisitor -- code review

Co-authored-by: Loïc DARDANT <loicdardant@MacBook-Pro-de-Loic.local>
2021-04-28 08:45:44 +02:00
John Nguyen 2524b62470 [Chore] Add SwiftLint (#252)
* Add swiftlint

* Fix trailing whitespace

* Fix vertical whitespace

* Fix trailing newline

* Fix opening brace

* Fix trailing comma

* Fix colon

* Fix comma

* Fix control statement

* Fix orphaned doc comment

* Fix void return

* Fix legacy constructor

* Fix unused closure parameter

* Fix type name

* Fix identifier name

* Configure large tuple

* Configure cyclomatic complexity

* Fix force try

* Fix xctfail message

* Fix force cast

* Fix test compilation

* Use snapshots for binding tests

* Fix line length

* Add swiftlint github action

* Clean up
2021-03-21 10:03:26 +01:00
John Nguyen f83e9a2566 [Chore] Fix codecov report (#251)
* Move snapshot tests to main scheme

* Update config files

* Fix conditions

* Bump test deployment version

* Fix ignored paths

* Fix ignored paths... again

* Try uploading coverage after each test
2021-03-19 22:07:04 +01:00
John Nguyen 3ac921e3a9 Update README.md 2021-02-28 15:30:47 +01:00
John Nguyen d127a624d3 [Changes] v0.10.0 2021-02-28 15:12:09 +01:00
103 changed files with 1449 additions and 871 deletions
+17
View File
@@ -0,0 +1,17 @@
name: SwiftLint
on:
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: GitHub Action for SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
+10
View File
@@ -0,0 +1,10 @@
included:
- Sources/Down
- Tests
large_tuple:
warning: 3
error: 4
cyclomatic_complexity:
ignores_case_statements: true
+5 -5
View File
@@ -4,9 +4,9 @@ before_install:
- set -o pipefail
- xcrun simctl boot "iPhone 12" || echo "(Pre)Launched the simulator."
script:
- travis_retry xcodebuild -project Down.xcodeproj -scheme "Down" -sdk iphonesimulator -destination "platform=iOS Simulator,OS=14.2,name=iPhone 12" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES test
- travis_retry xcodebuild -project Down.xcodeproj -scheme "Down" -sdk macosx -destination 'platform=OS X,arch=x86_64' -enableCodeCoverage YES test
- travis_retry xcodebuild -project Down.xcodeproj -scheme "Down" -sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV' -enableCodeCoverage YES test
- travis_retry xcodebuild -project Down.xcodeproj -scheme "DownSnapshotTests" -sdk iphonesimulator -destination "platform=iOS Simulator,OS=14.2,name=iPhone 12" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES test
after_success:
- travis_retry xcodebuild -project Down.xcodeproj -scheme "Down" -sdk iphonesimulator -destination "platform=iOS Simulator,OS=14.2,name=iPhone 12" -enableCodeCoverage YES ONLY_ACTIVE_ARCH=YES -quiet test
- bash <(curl -s https://codecov.io/bash)
- travis_retry xcodebuild -project Down.xcodeproj -scheme "Down" -sdk macosx -destination 'platform=OS X,arch=x86_64' -enableCodeCoverage YES -quiet test
- bash <(curl -s https://codecov.io/bash)
- travis_retry xcodebuild -project Down.xcodeproj -scheme "Down" -sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV' -enableCodeCoverage YES -quiet test
- bash <(curl -s https://codecov.io/bash)
+41 -2
View File
@@ -1,5 +1,41 @@
# Changelog
## [v0.11.0](https://github.com/johnxnguyen/Down/tree/v0.11.0) (2021-05-04)
[Full Changelog](https://github.com/johnxnguyen/Down/compare/v0.10.0...v0.11.0)
**Implemented enhancements:**
- \[CodeCoverage\] Improve accuracy for combined code coverage reporting [\#205](https://github.com/johnxnguyen/Down/issues/205)
**Closed issues:**
- DownStyler not included when installed via CocoaPods [\#254](https://github.com/johnxnguyen/Down/issues/254)
- \[Commonmark\] Strikethrough not working / not supported [\#253](https://github.com/johnxnguyen/Down/issues/253)
- \[Attributed Strings\] Unordered list items with a single line appear further indented than those with multiple lines when using a custom font [\#246](https://github.com/johnxnguyen/Down/issues/246)
**Merged pull requests:**
- \[Feature\] Custom list prefixes for AttributedStringVisitor [\#255](https://github.com/johnxnguyen/Down/pull/255) ([dloic](https://github.com/dloic))
- \[Chore\] Add SwiftLint [\#252](https://github.com/johnxnguyen/Down/pull/252) ([johnxnguyen](https://github.com/johnxnguyen))
- \[Chore\] Fix codecov report [\#251](https://github.com/johnxnguyen/Down/pull/251) ([johnxnguyen](https://github.com/johnxnguyen))
## [v0.10.0](https://github.com/johnxnguyen/Down/tree/v0.10.0) (2021-02-28)
[Full Changelog](https://github.com/johnxnguyen/Down/compare/v0.9.5...v0.10.0)
**Closed issues:**
- Does not build in 12.5 [\#244](https://github.com/johnxnguyen/Down/issues/244)
- \[Crash\] Missing resource bundle when using SPM [\#243](https://github.com/johnxnguyen/Down/issues/243)
**Merged pull requests:**
- \[Improvement\] Expose DownTextView's designated initializer [\#250](https://github.com/johnxnguyen/Down/pull/250) ([max-potapov](https://github.com/max-potapov))
- \[Chore\] Add arm64 as valid arch when building for simulator on M1 macs [\#249](https://github.com/johnxnguyen/Down/pull/249) ([michaelknoch](https://github.com/michaelknoch))
- \[Chore\] Reorganize project structure for SPM [\#248](https://github.com/johnxnguyen/Down/pull/248) ([johnxnguyen](https://github.com/johnxnguyen))
- \[Chore\] Use SPM to manage snapshot testing dependency [\#247](https://github.com/johnxnguyen/Down/pull/247) ([johnxnguyen](https://github.com/johnxnguyen))
## [v0.9.5](https://github.com/johnxnguyen/Down/tree/v0.9.5) (2021-02-12)
[Full Changelog](https://github.com/johnxnguyen/Down/compare/v0.9.4...v0.9.5)
@@ -71,6 +107,7 @@
**Merged pull requests:**
- Swift 5.1 Support [\#204](https://github.com/johnxnguyen/Down/pull/204) ([ghost](https://github.com/ghost))
- Resolves Swift Package Manager issue related to swift-snapshot-testing [\#203](https://github.com/johnxnguyen/Down/pull/203) ([ghost](https://github.com/ghost))
## [v0.9.1](https://github.com/johnxnguyen/Down/tree/v0.9.1) (2020-02-28)
@@ -99,7 +136,6 @@
**Merged pull requests:**
- Resolves Swift Package Manager issue related to swift-snapshot-testing [\#203](https://github.com/johnxnguyen/Down/pull/203) ([ghost](https://github.com/ghost))
- Improve configurability of DownStyler [\#188](https://github.com/johnxnguyen/Down/pull/188) ([mgacy](https://github.com/mgacy))
- Make color & font collection initializers public [\#184](https://github.com/johnxnguyen/Down/pull/184) ([johnxnguyen](https://github.com/johnxnguyen))
@@ -236,7 +272,6 @@
**Merged pull requests:**
- Wrap openURL so that Down compiles in app extensions [\#133](https://github.com/johnxnguyen/Down/pull/133) ([nheagy](https://github.com/nheagy))
- Fix Swift module errors when used alongside Firestore [\#125](https://github.com/johnxnguyen/Down/pull/125) ([vzsg](https://github.com/vzsg))
## [v0.6.5](https://github.com/johnxnguyen/Down/tree/v0.6.5) (2019-04-02)
@@ -255,6 +290,10 @@
- Support SwiftPM [\#128](https://github.com/johnxnguyen/Down/issues/128)
- \[DownView\] Load multiple .md files \(link support\) [\#68](https://github.com/johnxnguyen/Down/issues/68)
**Merged pull requests:**
- Fix Swift module errors when used alongside Firestore [\#125](https://github.com/johnxnguyen/Down/pull/125) ([vzsg](https://github.com/vzsg))
## [v0.6.3](https://github.com/johnxnguyen/Down/tree/v0.6.3) (2019-03-27)
[Full Changelog](https://github.com/johnxnguyen/Down/compare/v0.6.2...v0.6.3)
@@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "SnapshotTesting",
"repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git",
"state": {
"branch": null,
"revision": "c466812aa2e22898f27557e2e780d3aad7a27203",
"version": "1.8.2"
}
}
]
},
"version": 1
}
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |spec|
spec.name = "Down"
spec.summary = "Blazing fast Markdown rendering in Swift, built upon cmark."
spec.version = "0.10.0"
spec.version = "0.11.0"
spec.homepage = "https://github.com/johnxnguyen/Down"
spec.license = { :type => "MIT", :file => "LICENSE" }
spec.authors = { "John Nguyen" => "polyxo@protonmail.com" }
+59 -160
View File
@@ -66,8 +66,19 @@
907C64651EC133780095FEE1 /* TestDownView.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 907C64621EC120530095FEE1 /* TestDownView.bundle */; };
90A40A9C1EC03292004F2E91 /* Down.framework in Copy Bundled Frameworks */ = {isa = PBXBuildFile; fileRef = 8A569F401E6B3E50008BE2AC /* Down.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
EE0E54F82300800E0070C83F /* BlockBackgroundColorAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0E54F72300800E0070C83F /* BlockBackgroundColorAttribute.swift */; };
EE408A39230338B600E5278A /* CGPoint_TranslateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A38230338B600E5278A /* CGPoint_TranslateTests.swift */; };
EE408A3B2303399B00E5278A /* CGRect_HelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A3A2303399B00E5278A /* CGRect_HelpersTests.swift */; };
EE3E7E38260400E800170A52 /* DownDebugLayoutManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5D2DF822FC9E89009EC13E /* DownDebugLayoutManagerTests.swift */; };
EE3E7E3F260400EE00170A52 /* LinkStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5D2DF522FC99A6009EC13E /* LinkStyleTests.swift */; };
EE3E7E46260400F100170A52 /* CodeBlockStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE64722A22F8BB0B00C5F0BA /* CodeBlockStyleTests.swift */; };
EE3E7E4D260400F500170A52 /* BlockQuoteStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA2BDC722F705B900D0C72C /* BlockQuoteStyleTests.swift */; };
EE3E7E54260400F900170A52 /* ThematicBreakSyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5C02A22F58B8000B91D60 /* ThematicBreakSyleTests.swift */; };
EE3E7E5B260400FC00170A52 /* HeadingStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE99E3DD22F0C83F00BCE15B /* HeadingStyleTests.swift */; };
EE3E7E622604010000170A52 /* InlineStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9E886222EF76040005948C /* InlineStyleTests.swift */; };
EE3E7E692604010300170A52 /* ListItemStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE335C4922EDC85900648842 /* ListItemStyleTests.swift */; };
EE3E7E702604010700170A52 /* StylerTestSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE59C30122ECF0BD006EE8A8 /* StylerTestSuite.swift */; };
EE3E7E812604019300170A52 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = EE3E7E802604019300170A52 /* SnapshotTesting */; };
EE3E7E88260401F000170A52 /* VisitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE64FEEF225BEB3900A35B34 /* VisitorTests.swift */; };
EE408A39230338B600E5278A /* CGPointTranslateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A38230338B600E5278A /* CGPointTranslateTests.swift */; };
EE408A3B2303399B00E5278A /* CGRectHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A3A2303399B00E5278A /* CGRectHelpersTests.swift */; };
EE408A3D23033B6B00E5278A /* ListItemPrefixGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A3C23033B6B00E5278A /* ListItemPrefixGeneratorTests.swift */; };
EE44848B2301E51C0065C836 /* CodeBlockOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE44848A2301E51C0065C836 /* CodeBlockOptions.swift */; };
EE4484912301F2920065C836 /* CGPoint+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4484902301F2920065C836 /* CGPoint+Translate.swift */; };
@@ -86,19 +97,7 @@
EEA2BDCA22F7152B00D0C72C /* ThematicBreakOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA2BDC922F7152B00D0C72C /* ThematicBreakOptions.swift */; };
EEA5C02922F58A0900B91D60 /* DownTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5C02822F58A0900B91D60 /* DownTextView.swift */; };
EEA5C02D22F5C96B00B91D60 /* QuoteStripeAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5C02C22F5C96B00B91D60 /* QuoteStripeAttribute.swift */; };
EEBA15422344849600B54ECB /* Down.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A569F401E6B3E50008BE2AC /* Down.framework */; };
EEBA1546234484BF00B54ECB /* DownDebugLayoutManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5D2DF822FC9E89009EC13E /* DownDebugLayoutManagerTests.swift */; };
EEBA1547234484BF00B54ECB /* LinkStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5D2DF522FC99A6009EC13E /* LinkStyleTests.swift */; };
EEBA1548234484BF00B54ECB /* CodeBlockStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE64722A22F8BB0B00C5F0BA /* CodeBlockStyleTests.swift */; };
EEBA1549234484BF00B54ECB /* BlockQuoteStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA2BDC722F705B900D0C72C /* BlockQuoteStyleTests.swift */; };
EEBA154A234484BF00B54ECB /* ThematicBreakSyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5C02A22F58B8000B91D60 /* ThematicBreakSyleTests.swift */; };
EEBA154B234484BF00B54ECB /* HeadingStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE99E3DD22F0C83F00BCE15B /* HeadingStyleTests.swift */; };
EEBA154C234484BF00B54ECB /* InlineStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9E886222EF76040005948C /* InlineStyleTests.swift */; };
EEBA154D234484BF00B54ECB /* ListItemStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE335C4922EDC85900648842 /* ListItemStyleTests.swift */; };
EEBA154E234484BF00B54ECB /* StylerTestSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE59C30122ECF0BD006EE8A8 /* StylerTestSuite.swift */; };
EEBA154F234484C300B54ECB /* VisitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE64FEEF225BEB3900A35B34 /* VisitorTests.swift */; };
EEBE62F025E28F3D005CCAD6 /* BundleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBE62EF25E28F3D005CCAD6 /* BundleHelper.swift */; };
EED7FED725E303000033E33A /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = EED7FED625E303000033E33A /* SnapshotTesting */; };
EED7FED825E3133C0033E33A /* DownView (macOS).bundle in Resources */ = {isa = PBXBuildFile; fileRef = 14C5E33621877FCD00D5380C /* DownView (macOS).bundle */; };
EED8DA8E22BE404F00E54492 /* DownStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EED8DA8D22BE404F00E54492 /* DownStyler.swift */; };
EED8DA9022BECBAE00E54492 /* NSAttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EED8DA8F22BECBAE00E54492 /* NSAttributedString+Helpers.swift */; };
@@ -268,8 +267,9 @@
D4F948DB1D00A4A800C9C0F6 /* NSAttributedStringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAttributedStringTests.swift; sourceTree = "<group>"; };
EE0E54F72300800E0070C83F /* BlockBackgroundColorAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBackgroundColorAttribute.swift; sourceTree = "<group>"; };
EE335C4922EDC85900648842 /* ListItemStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemStyleTests.swift; sourceTree = "<group>"; };
EE408A38230338B600E5278A /* CGPoint_TranslateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPoint_TranslateTests.swift; sourceTree = "<group>"; };
EE408A3A2303399B00E5278A /* CGRect_HelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRect_HelpersTests.swift; sourceTree = "<group>"; };
EE39B28026063E0D002C4F8D /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
EE408A38230338B600E5278A /* CGPointTranslateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPointTranslateTests.swift; sourceTree = "<group>"; };
EE408A3A2303399B00E5278A /* CGRectHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectHelpersTests.swift; sourceTree = "<group>"; };
EE408A3C23033B6B00E5278A /* ListItemPrefixGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemPrefixGeneratorTests.swift; sourceTree = "<group>"; };
EE44848A2301E51C0065C836 /* CodeBlockOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeBlockOptions.swift; sourceTree = "<group>"; };
EE4484902301F2920065C836 /* CGPoint+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Translate.swift"; sourceTree = "<group>"; };
@@ -298,9 +298,6 @@
EEA5C02822F58A0900B91D60 /* DownTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownTextView.swift; sourceTree = "<group>"; };
EEA5C02A22F58B8000B91D60 /* ThematicBreakSyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThematicBreakSyleTests.swift; sourceTree = "<group>"; };
EEA5C02C22F5C96B00B91D60 /* QuoteStripeAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteStripeAttribute.swift; sourceTree = "<group>"; };
EEBA153A2344845500B54ECB /* DownSnapshotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DownSnapshotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EEBA153C2344845500B54ECB /* DownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownSnapshotTests.swift; sourceTree = "<group>"; };
EEBA153E2344845500B54ECB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
EEBE62EF25E28F3D005CCAD6 /* BundleHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BundleHelper.swift; sourceTree = "<group>"; };
EED8DA8D22BE404F00E54492 /* DownStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownStyler.swift; sourceTree = "<group>"; };
EED8DA8F22BECBAE00E54492 /* NSAttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Helpers.swift"; sourceTree = "<group>"; };
@@ -344,15 +341,7 @@
buildActionMask = 2147483647;
files = (
8AFAEB001E6E32E900E09B68 /* Down.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
EEBA15372344845500B54ECB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EEBA15422344849600B54ECB /* Down.framework in Frameworks */,
EED7FED725E303000033E33A /* SnapshotTesting in Frameworks */,
EE3E7E812604019300170A52 /* SnapshotTesting in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -398,10 +387,10 @@
D4201E761CFA5151008EEC6E = {
isa = PBXGroup;
children = (
EE39B28026063E0D002C4F8D /* .swiftlint.yml */,
D4201E9B1CFA59A5008EEC6E /* Sources */,
D41689B41CFFE6BB00E5802B /* Supporting Files */,
D4201EC41CFA59A5008EEC6E /* Tests */,
EEBA153B2344845500B54ECB /* DownSnapshotTests */,
D4201E811CFA5151008EEC6E /* Products */,
EE54F96D22EB9CE400628683 /* Frameworks */,
);
@@ -414,7 +403,6 @@
children = (
8A569F401E6B3E50008BE2AC /* Down.framework */,
8AFAEAFB1E6E32E900E09B68 /* DownTests.xctest */,
EEBA153A2344845500B54ECB /* DownSnapshotTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@@ -524,8 +512,8 @@
EE408A3E23033DFE00E5278A /* Helpers */ = {
isa = PBXGroup;
children = (
EE408A3A2303399B00E5278A /* CGRect_HelpersTests.swift */,
EE408A38230338B600E5278A /* CGPoint_TranslateTests.swift */,
EE408A3A2303399B00E5278A /* CGRectHelpersTests.swift */,
EE408A38230338B600E5278A /* CGPointTranslateTests.swift */,
EE97254222C14B79004D3B3A /* NSAttributedString+HelpersTests.swift */,
EE97253D22C130D8004D3B3A /* NSMutableAttributedString+AttributesTests.swift */,
);
@@ -632,15 +620,6 @@
path = Styling;
sourceTree = "<group>";
};
EEBA153B2344845500B54ECB /* DownSnapshotTests */ = {
isa = PBXGroup;
children = (
EEBA153C2344845500B54ECB /* DownSnapshotTests.swift */,
EEBA153E2344845500B54ECB /* Info.plist */,
);
path = DownSnapshotTests;
sourceTree = "<group>";
};
EEC752BE22C4AE1300EC729C /* AST */ = {
isa = PBXGroup;
children = (
@@ -796,6 +775,7 @@
8A569F3D1E6B3E50008BE2AC /* Headers */,
14C5E33421877CDC00D5380C /* Resources */,
14CEAC38218774BC00039EDF /* Replace Bundle for macOS Platform */,
EED185ED26054F800051E616 /* Swiftlint */,
);
buildRules = (
8AE66BE41F848C3900ED4C98 /* PBXBuildRule */,
@@ -824,30 +804,13 @@
8AFAEB021E6E32E900E09B68 /* PBXTargetDependency */,
);
name = DownTests;
packageProductDependencies = (
EE3E7E802604019300170A52 /* SnapshotTesting */,
);
productName = "DownTests-macOS";
productReference = 8AFAEAFB1E6E32E900E09B68 /* DownTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
EEBA15392344845500B54ECB /* DownSnapshotTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EEBA15412344845500B54ECB /* Build configuration list for PBXNativeTarget "DownSnapshotTests" */;
buildPhases = (
EEBA15362344845500B54ECB /* Sources */,
EEBA15372344845500B54ECB /* Frameworks */,
EEBA15382344845500B54ECB /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = DownSnapshotTests;
packageProductDependencies = (
EED7FED625E303000033E33A /* SnapshotTesting */,
);
productName = DownSnapshotTests;
productReference = EEBA153A2344845500B54ECB /* DownSnapshotTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -868,10 +831,6 @@
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
};
EEBA15392344845500B54ECB = {
CreatedOnToolsVersion = 11.0;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = D4201E7A1CFA5151008EEC6E /* Build configuration list for PBXProject "Down" */;
@@ -892,7 +851,6 @@
targets = (
8A569F3F1E6B3E50008BE2AC /* Down */,
8AFAEAFA1E6E32E900E09B68 /* DownTests */,
EEBA15392344845500B54ECB /* DownSnapshotTests */,
);
};
/* End PBXProject section */
@@ -915,13 +873,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
EEBA15382344845500B54ECB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -944,6 +895,24 @@
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nRESOURCE_PATH=$SRCROOT/Sources/Down/Resources\n\nFILENAME_IN_BUNDLE=DownView.bundle\n\nBUILD_APP_DIR=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework/Resources\n\necho \"$RESOURCE_PATH\"\necho \"$BUILD_APP_DIR\"\n\nif [ \"$PLATFORM_NAME\" == \"macosx\" ]; then\n echo $BUILD_APP_DIR\n rm -r \"$BUILD_APP_DIR/$FILENAME_IN_BUNDLE/\"\n cp -R \"$RESOURCE_PATH/DownView (macOS).bundle\" \"$BUILD_APP_DIR/$FILENAME_IN_BUNDLE/\"\nfi\n";
showEnvVarsInLog = 0;
};
EED185ED26054F800051E616 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -1042,36 +1011,29 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EE3E7E54260400F900170A52 /* ThematicBreakSyleTests.swift in Sources */,
EE3E7E4D260400F500170A52 /* BlockQuoteStyleTests.swift in Sources */,
EE97253E22C130D8004D3B3A /* NSMutableAttributedString+AttributesTests.swift in Sources */,
EE3E7E622604010000170A52 /* InlineStyleTests.swift in Sources */,
EE3E7E702604010700170A52 /* StylerTestSuite.swift in Sources */,
EE3E7E692604010300170A52 /* ListItemStyleTests.swift in Sources */,
EE3E7E46260400F100170A52 /* CodeBlockStyleTests.swift in Sources */,
EE97254322C14B79004D3B3A /* NSAttributedString+HelpersTests.swift in Sources */,
8AFAEB091E6E331700E09B68 /* StringTests.swift in Sources */,
EE3E7E5B260400FC00170A52 /* HeadingStyleTests.swift in Sources */,
EE408A3D23033B6B00E5278A /* ListItemPrefixGeneratorTests.swift in Sources */,
EE8F38CC22BFB2420056270E /* NodeTests.swift in Sources */,
EE408A39230338B600E5278A /* CGPoint_TranslateTests.swift in Sources */,
EE408A3B2303399B00E5278A /* CGRect_HelpersTests.swift in Sources */,
EE3E7E3F260400EE00170A52 /* LinkStyleTests.swift in Sources */,
EE3E7E38260400E800170A52 /* DownDebugLayoutManagerTests.swift in Sources */,
EE408A39230338B600E5278A /* CGPointTranslateTests.swift in Sources */,
EE408A3B2303399B00E5278A /* CGRectHelpersTests.swift in Sources */,
EE3E7E88260401F000170A52 /* VisitorTests.swift in Sources */,
8AFAEB071E6E331700E09B68 /* DownViewTests.swift in Sources */,
8AFAEB061E6E331700E09B68 /* BindingTests.swift in Sources */,
8AFAEB081E6E331700E09B68 /* NSAttributedStringTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
EEBA15362344845500B54ECB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EEBA154B234484BF00B54ECB /* HeadingStyleTests.swift in Sources */,
EEBA1549234484BF00B54ECB /* BlockQuoteStyleTests.swift in Sources */,
EEBA1546234484BF00B54ECB /* DownDebugLayoutManagerTests.swift in Sources */,
EEBA154D234484BF00B54ECB /* ListItemStyleTests.swift in Sources */,
EEBA154C234484BF00B54ECB /* InlineStyleTests.swift in Sources */,
EEBA154A234484BF00B54ECB /* ThematicBreakSyleTests.swift in Sources */,
EEBA1548234484BF00B54ECB /* CodeBlockStyleTests.swift in Sources */,
EEBA1547234484BF00B54ECB /* LinkStyleTests.swift in Sources */,
EEBA154E234484BF00B54ECB /* StylerTestSuite.swift in Sources */,
EEBA154F234484C300B54ECB /* VisitorTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -1137,9 +1099,11 @@
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
INFOPLIST_FILE = "Supporting Files/DownTests-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = com.downMarkdown.DownTests;
"TARGETED_DEVICE_FAMILY[sdk=appletvos*]" = 3;
"TARGETED_DEVICE_FAMILY[sdk=appletvsimulator*]" = 3;
TVOS_DEPLOYMENT_TARGET = 13.0;
};
name = Debug;
};
@@ -1153,9 +1117,11 @@
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
INFOPLIST_FILE = "Supporting Files/DownTests-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = com.downMarkdown.DownTests;
"TARGETED_DEVICE_FAMILY[sdk=appletvos*]" = 3;
"TARGETED_DEVICE_FAMILY[sdk=appletvsimulator*]" = 3;
TVOS_DEPLOYMENT_TARGET = 13.0;
};
name = Release;
};
@@ -1281,64 +1247,6 @@
};
name = Release;
};
EEBA153F2344845500B54ECB /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = DownSnapshotTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.downMarkdown.DownSnapshotTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
EEBA15402344845500B54ECB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = DownSnapshotTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.downMarkdown.DownSnapshotTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -1369,15 +1277,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EEBA15412344845500B54ECB /* Build configuration list for PBXNativeTarget "DownSnapshotTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EEBA153F2344845500B54ECB /* Debug */,
EEBA15402344845500B54ECB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
@@ -1392,7 +1291,7 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
EED7FED625E303000033E33A /* SnapshotTesting */ = {
EE3E7E802604019300170A52 /* SnapshotTesting */ = {
isa = XCSwiftPackageProductDependency;
package = EED7FED525E303000033E33A /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
productName = SnapshotTesting;
@@ -1,62 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EEBA15392344845500B54ECB"
BuildableName = "DownSnapshotTests.xctest"
BlueprintName = "DownSnapshotTests"
ReferencedContainer = "container:Down.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EEBA15392344845500B54ECB"
BuildableName = "DownSnapshotTests.xctest"
BlueprintName = "DownSnapshotTests"
ReferencedContainer = "container:Down.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
+1 -1
View File
@@ -1,7 +1,7 @@
## Down
[![Build Status](https://travis-ci.com/johnxnguyen/Down.svg?branch=master)](https://travis-ci.com/johnxnguyen/Down)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/johnxnguyen/Down/blob/master/LICENSE)
[![CocoaPods](https://img.shields.io/cocoapods/v/Down.svg?maxAge=10800)]()
[![CocoaPods](https://img.shields.io/cocoapods/v/Down)](https://cocoapods.org/pods/Down)
[![Swift 5](https://img.shields.io/badge/language-Swift-blue.svg)](https://swift.org)
[![macOS](https://img.shields.io/badge/OS-macOS-orange.svg)](https://developer.apple.com/macos/)
[![iOS](https://img.shields.io/badge/OS-iOS-orange.svg)](https://developer.apple.com/ios/)
+7 -3
View File
@@ -10,7 +10,9 @@ import Foundation
import libcmark
public class BaseNode: Node {
// MARK: - Properties
public let cmarkNode: CMarkNode
public private(set) lazy var children: [Node] = Array(childSequence)
@@ -25,9 +27,11 @@ public class BaseNode: Node {
}
return depth
}()
// MARK: - Life cycle
init(cmarkNode: CMarkNode) {
self.cmarkNode = cmarkNode
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class BlockQuote: BaseNode {}
// MARK: - Debug
extension BlockQuote: CustomDebugStringConvertible {
public var debugDescription: String {
return "Block Quote"
}
}
+24 -6
View File
@@ -7,13 +7,34 @@
import libcmark
/// Sequence of child nodes
/// Sequence of child nodes.
public struct ChildSequence: Sequence {
// MARK: - Properties
let node: CMarkNode
public struct Iterator: IteratorProtocol {
// MARK: - Methods
public func makeIterator() -> Iterator {
return Iterator(node: cmark_node_first_child(node))
}
}
// MARK: - Iterator
public extension ChildSequence {
struct Iterator: IteratorProtocol {
// MARK: - Properties
var node: CMarkNode?
// MARK: - Methods
public mutating func next() -> Node? {
guard let node = node else { return nil }
defer { self.node = cmark_node_next(node) }
@@ -25,10 +46,7 @@ public struct ChildSequence: Sequence {
return result
}
}
public func makeIterator() -> Iterator {
return Iterator(node: cmark_node_first_child(node))
}
}
+7 -3
View File
@@ -9,17 +9,21 @@ import Foundation
import libcmark
public class Code: BaseNode {
// MARK: - Properties
/// The code content, if present.
public private(set) lazy var literal: String? = cmarkNode.literal
}
// MARK: - Debug
extension Code: CustomDebugStringConvertible {
public var debugDescription: String {
return "Code - \(literal ?? "nil")"
}
}
+9 -4
View File
@@ -9,10 +9,13 @@ import Foundation
import libcmark
public class CodeBlock: BaseNode {
// MARK: - Properties
/// The code content, if present.
public private(set) lazy var literal: String? = cmarkNode.literal
/// The fence info is an optional string that trails the opening sequence of backticks.
/// It can be used to provide some contextual information about the block, such as
/// the name of a programming language.
@@ -24,16 +27,18 @@ public class CodeBlock: BaseNode {
/// '''
/// ```
///
public private(set) lazy var fenceInfo: String? = cmarkNode.fenceInfo
}
// MARK: - Debug
extension CodeBlock: CustomDebugStringConvertible {
public var debugDescription: String {
let content = (literal ?? "nil").replacingOccurrences(of: "\n", with: "\\n")
return "Code Block - fenceInfo: \(fenceInfo ?? "nil"), content: \(content)"
}
}
+7 -3
View File
@@ -9,17 +9,21 @@ import Foundation
import libcmark
public class CustomBlock: BaseNode {
// MARK: - Properfies
/// The custom content, if present.
public private(set) lazy var literal: String? = cmarkNode.literal
}
// MARK: - Debug
extension CustomBlock: CustomDebugStringConvertible {
public var debugDescription: String {
return "Custom Block - \(literal ?? "nil")"
}
}
+6 -2
View File
@@ -9,16 +9,20 @@ import Foundation
import libcmark
public class CustomInline: BaseNode {
// MARK: - Properties
/// The custom content, if present.
public private(set) lazy var literal: String? = cmarkNode.literal
}
// MARK: - Debug
extension CustomInline: CustomDebugStringConvertible {
public var debugDescription: String {
return "Custom Inline - \(literal ?? "nil")"
}
}
+10 -6
View File
@@ -9,26 +9,30 @@ import Foundation
import libcmark
public class Document: BaseNode {
// MARK: - Life cycle
deinit {
// Frees the node and all its children.
cmark_node_free(cmarkNode)
}
// MARK: - Methods
/// Accepts the given visitor and return its result.
@discardableResult
public func accept<T: Visitor>(_ visitor: T) -> T.Result {
return visitor.visit(document: self)
}
}
}
// MARK: - Debug
extension Document: CustomDebugStringConvertible {
public var debugDescription: String {
return "Document"
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class Emphasis: BaseNode {}
// MARK: - Debug
extension Emphasis: CustomDebugStringConvertible {
public var debugDescription: String {
return "Emphasis"
}
}
+6 -2
View File
@@ -9,16 +9,20 @@ import Foundation
import libcmark
public class Heading: BaseNode {
// MARK: - Properties
/// The level of the heading, a value between 1 and 6.
public private(set) lazy var headingLevel: Int = cmarkNode.headingLevel
}
// MARK: - Debug
extension Heading: CustomDebugStringConvertible {
public var debugDescription: String {
return "Heading - L\(headingLevel)"
}
}
+7 -3
View File
@@ -9,18 +9,22 @@ import Foundation
import libcmark
public class HtmlBlock: BaseNode {
// MARK: - Properties
/// The html content, if present.
public private(set) lazy var literal: String? = cmarkNode.literal
}
// MARK: - Debug
extension HtmlBlock: CustomDebugStringConvertible {
public var debugDescription: String {
let content = (literal ?? "nil").replacingOccurrences(of: "\n", with: "\\n")
return "Html Block - content: \(content)"
}
}
+7 -3
View File
@@ -9,17 +9,21 @@ import Foundation
import libcmark
public class HtmlInline: BaseNode {
// MARK: - Properties
/// The html tag, if present.
public private(set) lazy var literal: String? = cmarkNode.literal
}
// MARK: - Debug
extension HtmlInline: CustomDebugStringConvertible {
public var debugDescription: String {
return "Html Inline - \(literal ?? "nil")"
}
}
+9 -6
View File
@@ -9,7 +9,9 @@ import Foundation
import libcmark
public class Image: BaseNode {
// MARK: - Properties
/// The title of the image, if present.
///
/// In the example below, the first line is a reference link, with the reference at the
@@ -21,9 +23,9 @@ public class Image: BaseNode {
/// ...
/// [<id>]: <url> "<title>"
/// ```
///
public private(set) lazy var title: String? = cmarkNode.title
/// The url of the image, if present.
///
/// For example:
@@ -31,16 +33,17 @@ public class Image: BaseNode {
/// ```
/// ![<text>](<url>)
/// ```
///
public private(set) lazy var url: String? = cmarkNode.url
}
// MARK: - Debug
extension Image: CustomDebugStringConvertible {
public var debugDescription: String {
return "Image - title: \(title ?? "nil"), url: \(url ?? "nil"))"
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class Item: BaseNode {}
// MARK: - Debug
extension Item: CustomDebugStringConvertible {
public var debugDescription: String {
return "Item"
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class LineBreak: BaseNode {}
// MARK: - Debug
extension LineBreak: CustomDebugStringConvertible {
public var debugDescription: String {
return "Line Break"
}
}
+9 -6
View File
@@ -9,7 +9,9 @@ import Foundation
import libcmark
public class Link: BaseNode {
// MARK: - Properties
/// The title of the link, if present.
///
/// In the example below, the first line is a reference link, with the reference at the
@@ -21,9 +23,9 @@ public class Link: BaseNode {
/// ...
/// [<id>]: <url> "<title>"
/// ```
///
public private(set) lazy var title: String? = cmarkNode.title
/// The url of the link, if present.
///
/// For example:
@@ -31,16 +33,17 @@ public class Link: BaseNode {
/// ```
/// [<text>](<url>)
/// ```
///
public private(set) lazy var url: String? = cmarkNode.url
}
// MARK: - Debug
extension Link: CustomDebugStringConvertible {
public var debugDescription: String {
return "Link - title: \(title ?? "nil"), url: \(url ?? "nil"))"
}
}
+45 -4
View File
@@ -10,17 +10,21 @@ import libcmark
public class List: BaseNode {
// MARK: - Properties
/// The type of the list, either bullet or ordered.
public lazy var listType: ListType = {
guard let type = ListType(cmarkNode: cmarkNode) else {
assertionFailure("Unsupported or missing list type. Defaulting to .bullet.")
return .bullet
}
return type
}()
/// The number of items in the list.
public lazy var numberOfItems: Int = children.count
/// Whether the list is "tight".
@@ -29,16 +33,36 @@ public class List: BaseNode {
/// a hint to render the list with more (loose) or less (tight) spacing between items.
public lazy var isTight: Bool = cmark_node_get_list_tight(cmarkNode) == 1
/// The list delimiter.
public lazy var delimiter: Delimiter? = Delimiter(cmarkNode.listDelimiter)
}
// MARK: - List Type
public extension List {
enum Delimiter {
case period
case paren
init?(_ cmark: cmark_delim_type) {
switch cmark {
case CMARK_NO_DELIM: return nil
case CMARK_PERIOD_DELIM: self = .period
case CMARK_PAREN_DELIM: self = .paren
default: preconditionFailure("Invalid delim type")
}
}
}
enum ListType: CustomDebugStringConvertible {
case bullet
case ordered(start: Int)
// MARK: - Properties
public var debugDescription: String {
switch self {
case .bullet: return "Bullet"
@@ -46,6 +70,8 @@ public extension List {
}
}
// MARK: - Life cycle
init?(cmarkNode: CMarkNode) {
switch cmarkNode.listType {
case CMARK_BULLET_LIST: self = .bullet
@@ -53,14 +79,29 @@ public extension List {
default: return nil
}
}
}
}
// MARK: - Debug
extension List: CustomDebugStringConvertible {
public var debugDescription: String {
return "List - type: \(listType), isTight: \(isTight)"
var result = "List - type: \(listType), isTight: \(isTight)"
if let delim = delimiter {
result += ", delimiter: \(delim)"
}
return result
}
}
extension List.Delimiter: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .paren: return "paren"
case .period: return "period"
}
}
}
+26 -11
View File
@@ -10,33 +10,43 @@ import libcmark
/// A node is a wrapper of a raw `CMarkNode` belonging to the abstract syntax tree
/// generated by cmark.
public protocol Node {
/// The wrapped node.
var cmarkNode: CMarkNode { get }
/// The wrapped child nodes.
var children: [Node] { get }
}
public extension Node {
/// True iff the node has a sibling that succeeds it.
var hasSuccessor: Bool {
return cmark_node_next(cmarkNode) != nil
}
/// Sequence of wrapped child nodes.
var childSequence: ChildSequence {
return ChildSequence(node: cmarkNode)
}
}
// MARK: - Helper extensions
public typealias CMarkNode = UnsafeMutablePointer<cmark_node>
public extension UnsafeMutablePointer where Pointee == cmark_node {
public extension CMarkNode {
/// Wraps the cmark node referred to by this pointer.
func wrap() -> Node? {
switch type {
case CMARK_NODE_DOCUMENT: return Document(cmarkNode: self)
@@ -70,42 +80,47 @@ public extension UnsafeMutablePointer where Pointee == cmark_node {
var type: cmark_node_type {
return cmark_node_get_type(self)
}
var literal: String? {
return String(cString: cmark_node_get_literal(self))
}
var fenceInfo: String? {
return String(cString: cmark_node_get_fence_info(self))
}
var headingLevel: Int {
return Int(cmark_node_get_heading_level(self))
}
var listType: cmark_list_type {
return cmark_node_get_list_type(self)
}
var listStart: Int {
return Int(cmark_node_get_list_start(self))
}
var listDelimiter: cmark_delim_type {
return cmark_node_get_list_delim(self)
}
var url: String? {
return String(cString: cmark_node_get_url(self))
}
var title: String? {
return String(cString: cmark_node_get_title(self))
}
}
private extension String {
init?(cString: UnsafePointer<Int8>?) {
guard let unwrapped = cString else { return nil }
let result = String(cString: unwrapped)
guard !result.isEmpty else { return nil }
self = result
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class Paragraph: BaseNode {}
// MARK: - Debug
extension Paragraph: CustomDebugStringConvertible {
public var debugDescription: String {
return "Paragraph"
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class SoftBreak: BaseNode {}
// MARK: - Debug
extension SoftBreak: CustomDebugStringConvertible {
public var debugDescription: String {
return "Soft Break"
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class Strong: BaseNode {}
// MARK: - Debug
extension Strong: CustomDebugStringConvertible {
public var debugDescription: String {
return "Strong"
}
}
+7 -3
View File
@@ -9,17 +9,21 @@ import Foundation
import libcmark
public class Text: BaseNode {
// MARK: - Properties
/// The text content, if present.
public private(set) lazy var literal: String? = cmarkNode.literal
}
// MARK: - Debug
extension Text: CustomDebugStringConvertible {
public var debugDescription: String {
return "Text - \(literal ?? "nil")"
}
}
+2 -1
View File
@@ -13,8 +13,9 @@ public class ThematicBreak: BaseNode {}
// MARK: - Debug
extension ThematicBreak: CustomDebugStringConvertible {
public var debugDescription: String {
return "Thematic Break"
}
}
@@ -36,10 +36,13 @@ public protocol ColorCollection {
var thematicBreak: DownColor { get }
var listItemPrefix: DownColor { get }
var codeBlockBackground: DownColor { get }
}
public struct StaticColorCollection: ColorCollection {
// MARK: - Properties
public var heading1: DownColor
public var heading2: DownColor
public var heading3: DownColor
@@ -55,6 +58,8 @@ public struct StaticColorCollection: ColorCollection {
public var listItemPrefix: DownColor
public var codeBlockBackground: DownColor
// MARK: - Life cycle
public init(
heading1: DownColor = .black,
heading2: DownColor = .black,
@@ -86,6 +91,7 @@ public struct StaticColorCollection: ColorCollection {
self.listItemPrefix = listItemPrefix
self.codeBlockBackground = codeBlockBackground
}
}
#endif
@@ -31,10 +31,13 @@ public protocol FontCollection {
var body: DownFont { get }
var code: DownFont { get }
var listItemPrefix: DownFont { get }
}
public struct StaticFontCollection: FontCollection {
// MARK: - Properties
public var heading1: DownFont
public var heading2: DownFont
public var heading3: DownFont
@@ -45,6 +48,8 @@ public struct StaticFontCollection: FontCollection {
public var code: DownFont
public var listItemPrefix: DownFont
// MARK: - Life cycle
public init(
heading1: DownFont = .boldSystemFont(ofSize: 28),
heading2: DownFont = .boldSystemFont(ofSize: 24),
@@ -66,6 +71,7 @@ public struct StaticFontCollection: FontCollection {
self.code = code
self.listItemPrefix = listItemPrefix
}
}
#endif
@@ -28,10 +28,13 @@ public protocol ParagraphStyleCollection {
var heading6: NSParagraphStyle { get }
var body: NSParagraphStyle { get }
var code: NSParagraphStyle { get }
}
public struct StaticParagraphStyleCollection: ParagraphStyleCollection {
// MARK: - Properties
public var heading1: NSParagraphStyle
public var heading2: NSParagraphStyle
public var heading3: NSParagraphStyle
@@ -41,6 +44,8 @@ public struct StaticParagraphStyleCollection: ParagraphStyleCollection {
public var body: NSParagraphStyle
public var code: NSParagraphStyle
// MARK: - Life cycle
public init() {
let headingStyle = NSMutableParagraphStyle()
headingStyle.paragraphSpacing = 8
@@ -63,6 +68,7 @@ public struct StaticParagraphStyleCollection: ParagraphStyleCollection {
body = bodyStyle
code = codeStyle
}
}
#endif
@@ -20,13 +20,17 @@ import AppKit
struct BlockBackgroundColorAttribute {
// MARK: - Properties
var color: DownColor
var inset: CGFloat
}
extension NSAttributedString.Key {
static let blockBackgroundColor = NSAttributedString.Key("blockBackgroundColor")
}
#endif
@@ -20,6 +20,8 @@ import AppKit
struct QuoteStripeAttribute {
// MARK: - Properties
var color: DownColor
var thickness: CGFloat
var spacingAfter: CGFloat
@@ -28,25 +30,35 @@ struct QuoteStripeAttribute {
var layoutWidth: CGFloat {
return thickness + spacingAfter
}
}
extension QuoteStripeAttribute {
// MARK: - Life cycle
init(color: DownColor, thickness: CGFloat, spacingAfter: CGFloat, locations: [CGFloat]) {
self.color = color
self.thickness = thickness
self.spacingAfter = spacingAfter
self.locations = locations
}
init(level: Int, color: DownColor, options: QuoteStripeOptions) {
self.init(color: color, thickness: options.thickness, spacingAfter: options.spacingAfter, locations: [])
locations = (0..<level).map { CGFloat($0) * layoutWidth }
}
// MARK: - Methods
func indented(by indentation: CGFloat) -> QuoteStripeAttribute {
var copy = self
copy.locations = locations.map { $0 + indentation }
return copy
}
}
extension NSAttributedString.Key {
static let quoteStripe = NSAttributedString.Key(rawValue: "quoteStripe")
}
#endif
@@ -20,13 +20,17 @@ import AppKit
struct ThematicBreakAttribute {
// MARK: - Properties
var thickness: CGFloat
var color: DownColor
}
extension NSAttributedString.Key {
static let thematicBreak = NSAttributedString.Key(rawValue: "thematicBreak")
}
#endif
@@ -23,6 +23,7 @@ extension CGPoint {
func translated(by point: CGPoint) -> CGPoint {
return CGPoint(x: x + point.x, y: y + point.y)
}
}
#endif
@@ -27,6 +27,7 @@ extension CGRect {
func translated(by point: CGPoint) -> CGRect {
return CGRect(origin: origin.translated(by: point), size: size)
}
}
#endif
@@ -12,6 +12,8 @@ extension NSAttributedString {
typealias Attributes = [NSAttributedString.Key: Any]
// MARK: - Ranges
var wholeRange: NSRange {
return NSRange(location: 0, length: length)
}
@@ -32,11 +34,11 @@ extension NSAttributedString {
return ranges(for: key, in: range, where: { $0 == nil })
}
private func ranges(for key: Key, in range: NSRange, where p: (Any?) -> Bool) -> [NSRange] {
private func ranges(for key: Key, in range: NSRange, where predicate: (Any?) -> Bool) -> [NSRange] {
var ranges = [NSRange]()
enumerateAttribute(key, in: range, options: []) { value, attrRange, _ in
if p(value) {
if predicate(value) {
ranges.append(attrRange)
}
}
@@ -60,6 +62,8 @@ extension NSAttributedString {
return result.filter { $0.length > 1 }
}
// MARK: - Enumerate attributes
func enumerateAttributes<A>(for key: Key, block: (_ attr: A, _ range: NSRange) -> Void) {
enumerateAttributes(for: key, in: wholeRange, block: block)
}
@@ -71,4 +75,5 @@ extension NSAttributedString {
}
}
}
}
@@ -35,14 +35,14 @@ extension NSMutableAttributedString {
addAttribute(key, value: value, range: range)
}
func updateExistingAttributes<A>(for key: Key, using f: (A) -> A) {
updateExistingAttributes(for: key, in: wholeRange, using: f)
func updateExistingAttributes<A>(for key: Key, using transform: (A) -> A) {
updateExistingAttributes(for: key, in: wholeRange, using: transform)
}
func updateExistingAttributes<A>(for key: Key, in range: NSRange, using f: (A) -> A) {
func updateExistingAttributes<A>(for key: Key, in range: NSRange, using transform: (A) -> A) {
var existingValues = [(value: A, range: NSRange)]()
enumerateAttributes(for: key, in: range) { existingValues.append(($0, $1)) }
existingValues.forEach { addAttribute(key, value: f($0.0), range: $0.1) }
existingValues.forEach { addAttribute(key, value: transform($0.0), range: $0.1) }
}
func addAttributeInMissingRanges<A>(for key: Key, value: A) {
@@ -54,4 +54,5 @@ extension NSMutableAttributedString {
addAttribute(key, value: value, range: $0)
}
}
}
@@ -66,6 +66,7 @@ extension DownFont {
private func contains(_ trait: DownFontDescriptor.SymbolicTraits) -> Bool {
return fontDescriptor.symbolicTraits.contains(trait)
}
}
#if canImport(UIKit)
@@ -75,6 +76,7 @@ private extension DownFontDescriptor.SymbolicTraits {
static let strong = DownFontDescriptor.SymbolicTraits.traitBold
static let emphasis = DownFontDescriptor.SymbolicTraits.traitItalic
static let monoSpace = DownFontDescriptor.SymbolicTraits.traitMonoSpace
}
#elseif canImport(AppKit)
@@ -84,6 +86,7 @@ private extension DownFontDescriptor.SymbolicTraits {
static let strong = DownFontDescriptor.SymbolicTraits.bold
static let emphasis = DownFontDescriptor.SymbolicTraits.italic
static let monoSpace = DownFontDescriptor.SymbolicTraits.monoSpace
}
#endif
@@ -20,13 +20,17 @@ import AppKit
/// A convenient class used to format lists, such that list item prefixes
/// are right aligned and list item content left aligns.
public class ListItemParagraphStyler {
// MARK: - Properties
public var indentation: CGFloat {
return largestPrefixWidth + options.spacingAfterPrefix
}
/// The paragraph style intended for all paragraphs excluding the first.
public var trailingParagraphStyle: NSParagraphStyle {
let contentIndentation = indentation
let style = baseStyle
@@ -42,18 +46,23 @@ public class ListItemParagraphStyler {
let style = NSMutableParagraphStyle()
style.paragraphSpacingBefore = options.spacingAbove
style.paragraphSpacing = options.spacingBelow
style.alignment = options.alignment
return style
}
// MARK: - Life cycle
public init(options: ListItemOptions, prefixFont: DownFont) {
self.options = options
self.largestPrefixWidth = prefixFont.widthOfNumberedPrefix(digits: options.maxPrefixDigits)
}
// MARK: - Methods
/// The paragraph style intended for the first paragraph of the list item.
///
/// - Parameter prefixWidth: the width (in points) of the list item prefix.
public func leadingParagraphStyle(prefixWidth: CGFloat) -> NSParagraphStyle {
let contentIndentation = indentation
let prefixIndentation: CGFloat = contentIndentation - options.spacingAfterPrefix - prefixWidth
@@ -68,8 +77,9 @@ public class ListItemParagraphStyler {
}
private func tabStop(at location: CGFloat) -> NSTextTab {
return NSTextTab(textAlignment: .left, location: location, options: [:])
return NSTextTab(textAlignment: options.alignment, location: location, options: [:])
}
}
// MARK: - Helpers
@@ -91,6 +101,7 @@ private extension DownFont {
.size()
.width
}
}
private extension Int {
@@ -98,6 +109,7 @@ private extension Int {
static var decimalDigits: [Int] {
return Array(0...9)
}
}
#endif
@@ -31,15 +31,18 @@ import AppKit
/// of a `DownStyler`.
///
/// Insert this into a TextKit stack manually, or use the provided `DownDebugTextView`.
public class DownDebugLayoutManager: DownLayoutManager {
// MARK: - Drawing
override public func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
drawLineFragments(forGlyphRange: glyphsToShow, at: origin)
}
private func drawLineFragments(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
enumerateLineFragments(forGlyphRange: glyphsToShow) { rect, usedRect, textContainer, glyphRange, _ in
enumerateLineFragments(forGlyphRange: glyphsToShow) { rect, usedRect, _, _, _ in
[(usedRect, DownColor.blue), (rect, DownColor.red)].forEach { rectToDraw, color in
let adjustedRect = rectToDraw.translated(by: origin)
self.drawRect(adjustedRect, color: color.cgColor)
@@ -55,6 +58,7 @@ public class DownDebugLayoutManager: DownLayoutManager {
context.setStrokeColor(color)
context.stroke(rect)
}
}
#endif
@@ -21,8 +21,11 @@ import AppKit
/// A layout manager capable of drawing the custom attributes set by the `DownStyler`.
///
/// Insert this into a TextKit stack manually, or use the provided `DownTextView`.
public class DownLayoutManager: NSLayoutManager {
// MARK: - Graphic context
#if canImport(UIKit)
var context: CGContext? {
return UIGraphicsGetCurrentContext()
@@ -51,13 +54,15 @@ public class DownLayoutManager: NSLayoutManager {
#endif
// MARK: - Drawing
override public func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
drawCustomBackgrounds(forGlyphRange: glyphsToShow, at: origin)
super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
drawCustomAttributes(forGlyphRange: glyphsToShow, at: origin)
}
private func drawCustomBackgrounds(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
private func drawCustomBackgrounds(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
guard let context = context else { return }
push(context: context)
defer { popContext() }
@@ -66,17 +71,19 @@ public class DownLayoutManager: NSLayoutManager {
let characterRange = self.characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
textStorage.enumerateAttributes(for: .blockBackgroundColor, in: characterRange) { (attr: BlockBackgroundColorAttribute, blockRange) in
textStorage.enumerateAttributes(for: .blockBackgroundColor,
in: characterRange) { (attr: BlockBackgroundColorAttribute, blockRange) in
let inset = attr.inset
context.setFillColor(attr.color.cgColor)
let allBlockColorRanges = glyphRanges(for: .blockBackgroundColor, in: textStorage, inCharacterRange: blockRange)
let blockColorGlyphRange = glyphRange(forCharacterRange: blockRange, actualCharacterRange: nil)
let allBlockColorRanges = glyphRanges(for: .blockBackgroundColor,
in: textStorage,
inCharacterRange: blockRange)
enumerateLineFragments(forGlyphRange: blockColorGlyphRange) { lineRect, lineUsedRect, container, lineGlyphRange, _ in
let glyphRange = self.glyphRange(forCharacterRange: blockRange, actualCharacterRange: nil)
enumerateLineFragments(forGlyphRange: glyphRange) { lineRect, lineUsedRect, container, lineGlyphRange, _ in
let isLineStartOfBlock = allBlockColorRanges.contains {
lineGlyphRange.overlapsStart(of: $0)
}
@@ -89,7 +96,7 @@ public class DownLayoutManager: NSLayoutManager {
let maxX = lineRect.maxX
let minY = isLineStartOfBlock ? lineUsedRect.minY - inset : lineRect.minY
let maxY = isLineEndOfBlock ? lineUsedRect.maxY + inset : lineUsedRect.maxY
let blockRect = CGRect(minX: minX, minY: minY, maxX: maxX, maxY: maxY).translated(by: origin)
let blockRect = CGRect(minX: minX, minY: minY, maxX: maxX, maxY: maxY).translated(by: origin)
context.fill(blockRect)
}
@@ -107,7 +114,9 @@ public class DownLayoutManager: NSLayoutManager {
push(context: context)
defer { popContext() }
textStorage?.enumerateAttributes(for: .thematicBreak, in: characterRange) { (attr: ThematicBreakAttribute, range) in
textStorage?.enumerateAttributes(for: .thematicBreak,
in: characterRange) { (attr: ThematicBreakAttribute, range) in
let firstGlyphIndex = glyphIndexForCharacter(at: range.lowerBound)
let lineRect = lineFragmentRect(forGlyphAt: firstGlyphIndex, effectiveRange: nil)
@@ -115,7 +124,10 @@ public class DownLayoutManager: NSLayoutManager {
let lineStart = usedRect.minX + fragmentPadding(forGlyphAt: firstGlyphIndex)
let boundingRect = CGRect(x: lineStart, y: lineRect.minY, width: lineRect.width - lineStart, height: lineRect.height)
let width = lineRect.width - lineStart
let height = lineRect.height
let boundingRect = CGRect(x: lineStart, y: lineRect.minY, width: width, height: height)
let adjustedLineRect = boundingRect.translated(by: origin)
drawThematicBreak(with: context, in: adjustedLineRect, attr: attr)
@@ -140,7 +152,9 @@ public class DownLayoutManager: NSLayoutManager {
push(context: context)
defer { popContext() }
textStorage?.enumerateAttributes(for: .quoteStripe, in: characterRange) { (attr: QuoteStripeAttribute, quoteRange) in
textStorage?.enumerateAttributes(for: .quoteStripe,
in: characterRange) { (attr: QuoteStripeAttribute, quoteRange) in
context.setFillColor(attr.color.cgColor)
let glyphRangeOfQuote = self.glyphRange(forCharacterRange: quoteRange, actualCharacterRange: nil)
@@ -165,7 +179,10 @@ public class DownLayoutManager: NSLayoutManager {
}
}
private func glyphRanges(for key: NSAttributedString.Key, in storage: NSTextStorage, inCharacterRange range: NSRange) -> [NSRange] {
private func glyphRanges(for key: NSAttributedString.Key,
in storage: NSTextStorage,
inCharacterRange range: NSRange) -> [NSRange] {
return storage
.ranges(of: key, in: range)
.map { self.glyphRange(forCharacterRange: $0, actualCharacterRange: nil) }
@@ -184,6 +201,7 @@ private extension NSRange {
func overlapsEnd(of range: NSRange) -> Bool {
return lowerBound < range.upperBound && upperBound >= range.upperBound
}
}
private extension Array where Element == NSRange {
@@ -207,6 +225,7 @@ private extension Array where Element == NSRange {
return result
}
}
#endif
@@ -20,11 +20,16 @@ import AppKit
public struct CodeBlockOptions {
// MARK: - Properties
public var containerInset: CGFloat
// MARK: - Life cycle
public init(containerInset: CGFloat = 8) {
self.containerInset = containerInset
}
}
#endif
@@ -20,21 +20,29 @@ import AppKit
public struct ListItemOptions {
// MARK: - Properties
public var maxPrefixDigits: UInt
public var spacingAfterPrefix: CGFloat
public var spacingAbove: CGFloat
public var spacingBelow: CGFloat
public var alignment: NSTextAlignment
// MARK: - Life cycle
public init(maxPrefixDigits: UInt = 2,
spacingAfterPrefix: CGFloat = 8,
spacingAbove: CGFloat = 4,
spacingBelow: CGFloat = 8
) {
spacingBelow: CGFloat = 8,
alignment: NSTextAlignment = .natural) {
self.maxPrefixDigits = maxPrefixDigits
self.spacingAfterPrefix = spacingAfterPrefix
self.spacingAbove = spacingAbove
self.spacingBelow = spacingBelow
self.alignment = alignment
}
}
#endif
@@ -20,13 +20,18 @@ import AppKit
public struct QuoteStripeOptions {
// MARK: - Properties
public var thickness: CGFloat
public var spacingAfter: CGFloat
// MARK: - Life cycle
public init(thickness: CGFloat = 2, spacingAfter: CGFloat = 8) {
self.thickness = thickness
self.spacingAfter = spacingAfter
}
}
#endif
@@ -20,13 +20,18 @@ import AppKit
public struct ThematicBreakOptions {
// MARK: - Properties
public var thickness: CGFloat
public var indentation: CGFloat
// MARK: - Life cycle
public init(thickness: CGFloat = 1, indentation: CGFloat = 0) {
self.thickness = thickness
self.indentation = indentation
}
}
#endif
@@ -21,6 +21,7 @@ import AppKit
/// A default `Styler` implementation that supports a variety of configurable
/// properties for font, text color and paragraph styling, as well as formatting
/// of nested lists and quotes.
open class DownStyler: Styler {
// MARK: - Properties
@@ -35,12 +36,12 @@ open class DownStyler: Styler {
private let itemParagraphStyler: ListItemParagraphStyler
private var listPrefixAttributes: [NSAttributedString.Key : Any] {[
private var listPrefixAttributes: [NSAttributedString.Key: Any] {[
.font: fonts.listItemPrefix,
.foregroundColor: colors.listItemPrefix]
}
// MARK: - Init
// MARK: - Life cycle
public init(configuration: DownStylerConfiguration = DownStylerConfiguration()) {
fonts = configuration.fonts
@@ -49,7 +50,8 @@ open class DownStyler: Styler {
quoteStripeOptions = configuration.quoteStripeOptions
thematicBreakOptions = configuration.thematicBreakOptions
codeBlockOptions = configuration.codeBlockOptions
itemParagraphStyler = ListItemParagraphStyler(options: configuration.listItemOptions, prefixFont: fonts.listItemPrefix)
itemParagraphStyler = ListItemParagraphStyler(options: configuration.listItemOptions,
prefixFont: fonts.listItemPrefix)
}
// MARK: - Styling
@@ -59,7 +61,9 @@ open class DownStyler: Styler {
}
open func style(blockQuote str: NSMutableAttributedString, nestDepth: Int) {
let stripeAttribute = QuoteStripeAttribute(level: nestDepth + 1, color: colors.quoteStripe, options: quoteStripeOptions)
let stripeAttribute = QuoteStripeAttribute(level: nestDepth + 1,
color: colors.quoteStripe,
options: quoteStripeOptions)
str.updateExistingAttributes(for: .paragraphStyle) { (style: NSParagraphStyle) in
style.indented(by: stripeAttribute.layoutWidth)
@@ -82,7 +86,7 @@ open class DownStyler: Styler {
guard let leadingParagraphRange = paragraphRanges.first else { return }
indentListItemLeadingParagraph(in: str, prefixLength: prefixLength, inRange: leadingParagraphRange)
indentListItemLeadingParagraph(in: str, prefixLength: prefixLength, in: leadingParagraphRange)
paragraphRanges.dropFirst().forEach {
indentListItemTrailingParagraph(in: str, inRange: $0)
@@ -111,15 +115,15 @@ open class DownStyler: Styler {
str.updateExistingAttributes(for: .font) { (currentFont: DownFont) in
var newFont = font
if (currentFont.isMonospace) {
if currentFont.isMonospace {
newFont = newFont.monospace
}
if (currentFont.isEmphasized) {
if currentFont.isEmphasized {
newFont = newFont.emphasis
}
if (currentFont.isStrong) {
if currentFont.isStrong {
newFont = newFont.strong
}
@@ -229,7 +233,10 @@ open class DownStyler: Styler {
}
}
private func indentListItemLeadingParagraph(in str: NSMutableAttributedString, prefixLength: Int, inRange range: NSRange) {
private func indentListItemLeadingParagraph(in str: NSMutableAttributedString,
prefixLength: Int,
in range: NSRange) {
str.updateExistingAttributes(for: .paragraphStyle, in: range) { (existingStyle: NSParagraphStyle) in
existingStyle.indented(by: itemParagraphStyler.indentation)
}
@@ -265,7 +272,7 @@ open class DownStyler: Styler {
private extension NSParagraphStyle {
func indented(by indentation: CGFloat) -> NSParagraphStyle {
let result = mutableCopy() as! NSMutableParagraphStyle
guard let result = mutableCopy() as? NSMutableParagraphStyle else { return self }
result.firstLineHeadIndent += indentation
result.headIndent += indentation
@@ -277,7 +284,7 @@ private extension NSParagraphStyle {
}
func inset(by amount: CGFloat) -> NSParagraphStyle {
let result = mutableCopy() as! NSMutableParagraphStyle
guard let result = mutableCopy() as? NSMutableParagraphStyle else { return self }
result.paragraphSpacingBefore += amount
result.paragraphSpacing += amount
result.firstLineHeadIndent += amount
@@ -285,6 +292,7 @@ private extension NSParagraphStyle {
result.tailIndent = -amount
return result
}
}
private extension NSAttributedString {
@@ -292,8 +300,9 @@ private extension NSAttributedString {
func prefix(with length: Int) -> NSAttributedString {
guard length <= self.length else { return self }
guard length > 0 else { return NSAttributedString() }
return attributedSubstring(from: NSMakeRange(0, length))
return attributedSubstring(from: NSRange(location: 0, length: length))
}
}
#endif
@@ -9,17 +9,22 @@
#if !os(watchOS) && !os(Linux)
/// A configuration object used to initialze the `DownStyler`.
public struct DownStylerConfiguration {
// MARK: - Properties
public var fonts: FontCollection
public var colors: ColorCollection
public var paragraphStyles: ParagraphStyleCollection
public var listItemOptions: ListItemOptions
public var quoteStripeOptions: QuoteStripeOptions
public var thematicBreakOptions: ThematicBreakOptions
public var codeBlockOptions: CodeBlockOptions
// MARK: - Life cycle
public init(fonts: FontCollection = StaticFontCollection(),
colors: ColorCollection = StaticColorCollection(),
paragraphStyles: ParagraphStyleCollection = StaticParagraphStyleCollection(),
@@ -36,6 +41,7 @@ public struct DownStylerConfiguration {
self.thematicBreakOptions = thematicBreakOptions
self.codeBlockOptions = codeBlockOptions
}
}
#endif
+74 -49
View File
@@ -18,147 +18,172 @@ import Foundation
///
/// A styler is used in conjunction with an instance of `AttributedStringVisitor` in order
/// to generate an NSAttributedString from an abstract syntax tree.
public protocol Styler {
/// Styles the content of the document in the given string.
///
/// - Parameter str: the document content.
func style(document str: NSMutableAttributedString)
/// - Parameters:
/// - str: the document content.
func style(document str: NSMutableAttributedString)
/// Styles the content of the block quote contained in the given string.
///
/// - Parameter str: the quote content.
/// - Parameter nestDepth: the zero indexed nesting depth of the block quote node.
func style(blockQuote str: NSMutableAttributedString, nestDepth: Int)
/// - Parameters:
/// - str: the quote content.
/// - nestDepth: the zero indexed nesting depth of the block quote node.
func style(blockQuote str: NSMutableAttributedString, nestDepth: Int)
/// Styles the content of the list contained in the given string.
///
/// - Parameter str: the list content.
/// - Parameter nestDepth: the zero indexed nesting depth of the list node.
func style(list str: NSMutableAttributedString, nestDepth: Int)
/// - Parameters:
/// - str: the list content.
/// - nestDepth: the zero indexed nesting depth of the list node.
func style(list str: NSMutableAttributedString, nestDepth: Int)
/// Styles the number or bullet list item prefix.
///
/// - Parameter str: the list item prefix.
func style(listItemPrefix str: NSMutableAttributedString)
/// - Parameters:
/// - str: the list item prefix.
func style(listItemPrefix str: NSMutableAttributedString)
/// Styles the content of the list item contained in the given string, including the
/// number or bullet prefix.
///
/// - Parameter str: the item content.
/// - Parameter prefixLength: the character length of the number or bullet prefix.
func style(item str: NSMutableAttributedString, prefixLength: Int)
/// - Parameters:
/// - str: the item content.
/// - prefixLength: the character length of the number or bullet prefix.
func style(item str: NSMutableAttributedString, prefixLength: Int)
/// Styles the content of the code block in the given string.
///
/// An example use case for `fenceInfo` is to specify a programming language name,
/// which could be used to support syntax highlighting.
///
/// - Parameter str: the code content.
/// - Parameter fenceInfo: the string that trails the initial ``` ticks.
func style(codeBlock str: NSMutableAttributedString, fenceInfo: String?)
/// - Parameters:
/// - str: the code content.
/// - fenceInfo: the string that trails the initial \`\`\` ticks.
func style(codeBlock str: NSMutableAttributedString, fenceInfo: String?)
/// Styles the content of the html block contained in the given string.
///
/// - Parameter str: the html content.
func style(htmlBlock str: NSMutableAttributedString)
/// - Parameters:
/// - str: the html content.
func style(htmlBlock str: NSMutableAttributedString)
/// Styles the content of the custom block contained in the given string.
///
/// - Parameter str: the content.
func style(customBlock str: NSMutableAttributedString)
/// - Parameters:
/// - str: the content.
func style(customBlock str: NSMutableAttributedString)
/// Styles the content of the paragraph in the given string.
///
/// - Parameter str: the paragraph content.
func style(paragraph str: NSMutableAttributedString)
/// - Parameters:
/// - str: the paragraph content.
func style(paragraph str: NSMutableAttributedString)
/// Styles the content of the heading in the given string.
///
/// - Parameter str: the heading content.
/// - Parameter level: the heading level [1, 6]
func style(heading str: NSMutableAttributedString, level: Int)
/// - Parameters:
/// - str: the heading content.
/// - level: the heading level [1, 6]
func style(heading str: NSMutableAttributedString, level: Int)
/// Styles the content of the thematic break in the given string.
///
/// - Parameter str: the thematic break.
func style(thematicBreak str: NSMutableAttributedString)
/// - Parameters:
/// - str: the thematic break.
func style(thematicBreak str: NSMutableAttributedString)
/// Styles the content of the inline text node in the given string.
///
/// The text nodes are always the leaves of the AST, thus they
/// contain the base style upon which other nodes can work with.
///
/// - Parameter str: the text content.
func style(text str: NSMutableAttributedString)
/// - Parameters:
/// - str: the text content.
func style(text str: NSMutableAttributedString)
/// Styles the content of the soft break in the given string.
///
/// - Parameter str: the soft break.
func style(softBreak str: NSMutableAttributedString)
/// - Parameters:
/// - str: the soft break.
func style(softBreak str: NSMutableAttributedString)
/// Styles the content of the line break in the given string.
///
/// - Parameter str: the line break.
func style(lineBreak str: NSMutableAttributedString)
/// - Parameters:
/// - str: the line break.
func style(lineBreak str: NSMutableAttributedString)
/// Styles the content of the inline code in the given string.
///
/// - Parameter str: the code content.
func style(code str: NSMutableAttributedString)
/// - Parameters:
/// - str: the code content.
func style(code str: NSMutableAttributedString)
/// Styles the content of the inline html tags in the given string.
///
/// Note, the content does not include text between matching tags.
///
/// - Parameter str: the html content.
func style(htmlInline str: NSMutableAttributedString)
/// - Parameters:
/// - str: the html content.
func style(htmlInline str: NSMutableAttributedString)
/// Styles the content of the inline custom node in the given string.
///
/// - Parameter str: the custom content.
func style(customInline str: NSMutableAttributedString)
/// - Parameters:
/// - str: the custom content.
func style(customInline str: NSMutableAttributedString)
/// Styles the content of the inline emphasis node in the given string.
///
/// - Parameter str: the ephasized content.
func style(emphasis str: NSMutableAttributedString)
/// - Parameters:
/// - str: the ephasized content.
func style(emphasis str: NSMutableAttributedString)
/// Styles the content of the inline strong node in the given string.
///
/// - Parameter str: the strong content.
func style(strong str: NSMutableAttributedString)
/// - Parameters:
/// - str: the strong content.
func style(strong str: NSMutableAttributedString)
/// Styles the content of the inline link node in the given string.
///
/// - Parameter str: the link content.
/// - Parameter title: the link title.
/// - Parameter url: the linked url.
/// - Parameters:
/// - str: the link content.
/// - title: the link title.
/// - url: the linked url.
func style(link str: NSMutableAttributedString, title: String?, url: String?)
/// Styles the content of the inline image node in the given string.
///
/// - Parameter str: the link content.
/// - Parameter title: the link title.
/// - Parameter url: the linked url.
/// - Parameters:
/// - str: the link content.
/// - title: the link title.
/// - url: the linked url.
func style(image str: NSMutableAttributedString, title: String?, url: String?)
}
@@ -21,8 +21,11 @@ import AppKit
/// A text view capable of parsing and rendering markdown via the AST, as well as line fragments.
///
/// See `DownDebugLayoutManager`.
public class DownDebugTextView: DownTextView {
// MARK: - Life cycle
public init(frame: CGRect, styler: Styler = DownStyler()) {
super.init(frame: frame, styler: styler, layoutManager: DownDebugLayoutManager())
}
@@ -30,6 +33,7 @@ public class DownDebugTextView: DownTextView {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#endif
@@ -21,11 +21,16 @@ public typealias TextView = NSTextView
#endif
/// A text view capable of parsing and rendering markdown via the AST.
open class DownTextView: TextView {
// MARK: - Properties
open var styler: Styler
open var styler: Styler {
didSet {
try? render()
}
}
#if canImport(UIKit)
@@ -40,15 +45,14 @@ open class DownTextView: TextView {
open override var string: String {
didSet {
guard oldValue != string else { return }
guard oldValue != string else { return }
try? render()
}
}
#endif
// MARK: - Init
// MARK: - Life cycle
public convenience init(frame: CGRect, styler: Styler = DownStyler()) {
self.init(frame: frame, styler: styler, layoutManager: DownLayoutManager())
@@ -90,6 +94,7 @@ open class DownTextView: TextView {
#endif
}
}
#endif
@@ -13,10 +13,16 @@ import Foundation
/// tree produced by a markdown string. It traverses the tree to construct substrings
/// represented at each node and uses an instance of `Styler` to apply the visual attributes.
/// These substrings are joined together to produce the final result.
public typealias ListPrefixGeneratorBuilder = (List) -> ListItemPrefixGenerator
public class AttributedStringVisitor {
// MARK: - Properties
private let styler: Styler
private let options: DownOptions
private let listPrefixGeneratorBuilder: ListPrefixGeneratorBuilder
private var listPrefixGenerators = [ListItemPrefixGenerator]()
/// Creates a new instance with the given styler and options.
@@ -24,153 +30,163 @@ public class AttributedStringVisitor {
/// - parameters:
/// - styler: used to style the markdown elements.
/// - options: may be used to modify rendering.
public init(styler: Styler, options: DownOptions = .default) {
/// - listPrefixGeneratorBuilder: may be used to modify list prefixes.
public init(
styler: Styler,
options: DownOptions = .default,
listPrefixGeneratorBuilder: @escaping ListPrefixGeneratorBuilder = { StaticListItemPrefixGenerator(list: $0) }
) {
self.styler = styler
self.options = options
self.listPrefixGeneratorBuilder = listPrefixGeneratorBuilder
}
}
extension AttributedStringVisitor: Visitor {
public typealias Result = NSMutableAttributedString
public func visit(document node: Document) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
styler.style(document: s)
return s
let result = visitChildren(of: node).joined
styler.style(document: result)
return result
}
public func visit(blockQuote node: BlockQuote) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
if node.hasSuccessor { s.append(.paragraphSeparator) }
styler.style(blockQuote: s, nestDepth: node.nestDepth)
return s
let result = visitChildren(of: node).joined
if node.hasSuccessor { result.append(.paragraphSeparator) }
styler.style(blockQuote: result, nestDepth: node.nestDepth)
return result
}
public func visit(list node: List) -> NSMutableAttributedString {
listPrefixGenerators.append(ListItemPrefixGenerator(list: node))
listPrefixGenerators.append(listPrefixGeneratorBuilder(node))
defer { listPrefixGenerators.removeLast() }
let items = visitChildren(of: node)
let s = items.joined
if node.hasSuccessor { s.append(.paragraphSeparator) }
styler.style(list: s, nestDepth: node.nestDepth)
return s
let result = items.joined
if node.hasSuccessor { result.append(.paragraphSeparator) }
styler.style(list: result, nestDepth: node.nestDepth)
return result
}
public func visit(item node: Item) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
let result = visitChildren(of: node).joined
let prefix = listPrefixGenerators.last?.next() ?? ""
let attributedPrefix = "\(prefix)\t".attributed
styler.style(listItemPrefix: attributedPrefix)
s.insert(attributedPrefix, at: 0)
result.insert(attributedPrefix, at: 0)
if node.hasSuccessor { s.append(.paragraphSeparator) }
styler.style(item: s, prefixLength: (prefix as NSString).length)
return s
if node.hasSuccessor { result.append(.paragraphSeparator) }
styler.style(item: result, prefixLength: (prefix as NSString).length)
return result
}
public func visit(codeBlock node: CodeBlock) -> NSMutableAttributedString {
guard let literal = node.literal else { return .empty }
let s = literal.replacingNewlinesWithLineSeparators().attributed
if node.hasSuccessor { s.append(.paragraphSeparator) }
styler.style(codeBlock: s, fenceInfo: node.fenceInfo)
return s
let result = literal.replacingNewlinesWithLineSeparators().attributed
if node.hasSuccessor { result.append(.paragraphSeparator) }
styler.style(codeBlock: result, fenceInfo: node.fenceInfo)
return result
}
public func visit(htmlBlock node: HtmlBlock) -> NSMutableAttributedString {
guard let literal = node.literal else { return .empty }
let s = literal.replacingNewlinesWithLineSeparators().attributed
if node.hasSuccessor { s.append(.paragraphSeparator) }
styler.style(htmlBlock: s)
return s
let result = literal.replacingNewlinesWithLineSeparators().attributed
if node.hasSuccessor { result.append(.paragraphSeparator) }
styler.style(htmlBlock: result)
return result
}
public func visit(customBlock node: CustomBlock) -> NSMutableAttributedString {
guard let s = node.literal?.attributed else { return .empty }
styler.style(customBlock: s)
return s
guard let result = node.literal?.attributed else { return .empty }
styler.style(customBlock: result)
return result
}
public func visit(paragraph node: Paragraph) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
if node.hasSuccessor { s.append(.paragraphSeparator) }
styler.style(paragraph: s)
return s
let result = visitChildren(of: node).joined
if node.hasSuccessor { result.append(.paragraphSeparator) }
styler.style(paragraph: result)
return result
}
public func visit(heading node: Heading) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
if node.hasSuccessor { s.append(.paragraphSeparator) }
styler.style(heading: s, level: node.headingLevel)
return s
let result = visitChildren(of: node).joined
if node.hasSuccessor { result.append(.paragraphSeparator) }
styler.style(heading: result, level: node.headingLevel)
return result
}
public func visit(thematicBreak node: ThematicBreak) -> NSMutableAttributedString {
let s = "\(String.zeroWidthSpace)\n".attributed
styler.style(thematicBreak: s)
return s
let result = "\(String.zeroWidthSpace)\n".attributed
styler.style(thematicBreak: result)
return result
}
public func visit(text node: Text) -> NSMutableAttributedString {
guard let s = node.literal?.attributed else { return .empty }
styler.style(text: s)
return s
guard let result = node.literal?.attributed else { return .empty }
styler.style(text: result)
return result
}
public func visit(softBreak node: SoftBreak) -> NSMutableAttributedString {
let s = (options.contains(.hardBreaks) ? String.lineSeparator : " ").attributed
styler.style(softBreak: s)
return s
let result = (options.contains(.hardBreaks) ? String.lineSeparator : " ").attributed
styler.style(softBreak: result)
return result
}
public func visit(lineBreak node: LineBreak) -> NSMutableAttributedString {
let s = String.lineSeparator.attributed
styler.style(lineBreak: s)
return s
let result = String.lineSeparator.attributed
styler.style(lineBreak: result)
return result
}
public func visit(code node: Code) -> NSMutableAttributedString {
guard let s = node.literal?.attributed else { return .empty }
styler.style(code: s)
return s
guard let result = node.literal?.attributed else { return .empty }
styler.style(code: result)
return result
}
public func visit(htmlInline node: HtmlInline) -> NSMutableAttributedString {
guard let s = node.literal?.attributed else { return .empty }
styler.style(htmlInline: s)
return s
guard let result = node.literal?.attributed else { return .empty }
styler.style(htmlInline: result)
return result
}
public func visit(customInline node: CustomInline) -> NSMutableAttributedString {
guard let s = node.literal?.attributed else { return .empty }
styler.style(customInline: s)
return s
guard let result = node.literal?.attributed else { return .empty }
styler.style(customInline: result)
return result
}
public func visit(emphasis node: Emphasis) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
styler.style(emphasis: s)
return s
let result = visitChildren(of: node).joined
styler.style(emphasis: result)
return result
}
public func visit(strong node: Strong) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
styler.style(strong: s)
return s
let result = visitChildren(of: node).joined
styler.style(strong: result)
return result
}
public func visit(link node: Link) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
styler.style(link: s, title: node.title, url: node.url)
return s
let result = visitChildren(of: node).joined
styler.style(link: result, title: node.title, url: node.url)
return result
}
public func visit(image node: Image) -> NSMutableAttributedString {
let s = visitChildren(of: node).joined
styler.style(image: s, title: node.title, url: node.url)
return s
let result = visitChildren(of: node).joined
styler.style(image: result, title: node.title, url: node.url)
return result
}
}
@@ -181,6 +197,7 @@ private extension Sequence where Iterator.Element == NSMutableAttributedString {
var joined: NSMutableAttributedString {
return reduce(into: NSMutableAttributedString()) { $0.append($1) }
}
}
private extension NSMutableAttributedString {
@@ -188,6 +205,7 @@ private extension NSMutableAttributedString {
static var empty: NSMutableAttributedString {
return "".attributed
}
}
private extension NSAttributedString {
@@ -195,6 +213,7 @@ private extension NSAttributedString {
static var paragraphSeparator: NSAttributedString {
return String.paragraphSeparator.attributed
}
}
private extension String {
@@ -204,11 +223,13 @@ private extension String {
}
// This codepoint marks the end of a paragraph and the start of the next.
static var paragraphSeparator: String {
return "\u{2029}"
}
// This code point allows line breaking, without starting a new paragraph.
static var lineSeparator: String {
return "\u{2028}"
}
@@ -222,5 +243,7 @@ private extension String {
let lines = trimmed.components(separatedBy: .newlines)
return lines.joined(separator: .lineSeparator)
}
}
#endif // !os(Linux)
+9 -2
View File
@@ -9,16 +9,23 @@ import Foundation
/// This visitor will generate the debug description of an entire abstract syntax tree,
/// indicating relationships between nodes with indentation.
public class DebugVisitor: Visitor {
// MARK: - Properties
private var depth = 0
private var indent: String {
return String(repeating: " ", count: depth)
}
// MARK: - Life cycle
public init() {}
// MARK: - Helpers
private func report(_ node: Node) -> String {
return "\(indent)\(node is Document ? "" : "")\(String(reflecting: node))\n"
}
@@ -114,5 +121,5 @@ public class DebugVisitor: Visitor {
public func visit(image node: Image) -> String {
return reportWithChildren(node)
}
}
}
@@ -8,28 +8,47 @@
import Foundation
class ListItemPrefixGenerator {
/// A ListItemPrefixGenerator is an object used to generate list item prefix.
public protocol ListItemPrefixGenerator {
init(listType: List.ListType, numberOfItems: Int, nestDepth: Int)
func next() -> String?
}
public extension ListItemPrefixGenerator {
init(list: List) {
self.init(listType: list.listType, numberOfItems: list.numberOfItems, nestDepth: list.nestDepth)
}
}
/// Default implementation of `ListItemPrefixGenerator`.
/// Generating the following symbol based on `List.ListType`:
/// - List.ListType is bullet => ""
/// - List.ListType is ordered => "X." (where is the item number)
public class StaticListItemPrefixGenerator: ListItemPrefixGenerator {
// MARK: - Properties
private var prefixes: IndexingIterator<[String]>
convenience init(list: List) {
self.init(listType: list.listType, numberOfItems: list.numberOfItems)
}
// MARK: - Life cycle
init(listType: List.ListType, numberOfItems: Int) {
switch listType {
case .bullet:
prefixes = [String](repeating: "", count: numberOfItems)
.makeIterator()
required public init(listType: List.ListType, numberOfItems: Int, nestDepth: Int) {
switch listType {
case .bullet:
prefixes = [String](repeating: "", count: numberOfItems)
.makeIterator()
case .ordered(let start):
prefixes = (start..<(start + numberOfItems))
.map { "\($0)." }
.makeIterator()
case .ordered(let start):
prefixes = (start..<(start + numberOfItems))
.map { "\($0)." }
.makeIterator()
}
}
}
func next() -> String? {
// MARK: - Methods
public func next() -> String? {
prefixes.next()
}
}
+6
View File
@@ -11,8 +11,11 @@ import Foundation
/// each node of the tree and produces some result for that node. A visitor is "accepted" by
/// the root node (of type `Document`), which will start the traversal by first invoking
/// `visit(document:)`.
public protocol Visitor {
associatedtype Result
func visit(document node: Document) -> Result
func visit(blockQuote node: BlockQuote) -> Result
func visit(list node: List) -> Result
@@ -34,9 +37,11 @@ public protocol Visitor {
func visit(link node: Link) -> Result
func visit(image node: Image) -> Result
func visitChildren(of node: Node) -> [Result]
}
extension Visitor {
public func visitChildren(of node: Node) -> [Result] {
return node.childSequence.compactMap { child in
switch child {
@@ -66,4 +71,5 @@ extension Visitor {
}
}
}
}
+2 -1
View File
@@ -13,7 +13,8 @@ public struct Down: DownASTRenderable, DownHTMLRenderable, DownXMLRenderable,
/// A string containing CommonMark Markdown
public var markdownString: String
/// Initializes the container with a CommonMark Markdown string which can then be rendered depending on protocol conformance
/// Initializes the container with a CommonMark Markdown string which can then be
/// rendered depending on protocol conformance.
///
/// - Parameter markdownString: A string containing CommonMark Markdown
public init(markdownString: String) {
+13 -4
View File
@@ -9,19 +9,28 @@
import Foundation
public enum DownErrors: Error {
/// Thrown when there was an issue converting the Markdown into an abstract syntax tree
/// Thrown when there was an issue converting the Markdown into an abstract syntax tree.
case markdownToASTError
/// Thrown when the abstract syntax tree could not be rendered into another format
/// Thrown when the abstract syntax tree could not be rendered into another format.
case astRenderingError
/// Thrown when an HTML string cannot be converted into an `NSData` representation
/// Thrown when an HTML string cannot be converted into an `NSData` representation.
case htmlDataConversionError
#if os(macOS)
/// Thrown when a custom template bundle has a non-standard bundle format.
///
/// Specifically, the file URL of the bundles subdirectory containing resource files could not be found (i.e. the bundle's `resourceURL` property is nil).
/// Specifically, the file URL of the bundles subdirectory containing resource files could
/// not be found (i.e. the bundle's `resourceURL` property is nil).
case nonStandardBundleFormatError
#endif
}
+20 -5
View File
@@ -10,18 +10,27 @@ import Foundation
import libcmark
public struct DownOptions: OptionSet {
// MARK: - Properties
public let rawValue: Int32
// MARK: - Life cycle
public init(rawValue: Int32) { self.rawValue = rawValue }
/// Default options
/// Default options.
public static let `default` = DownOptions(rawValue: CMARK_OPT_DEFAULT)
// MARK: - Rendering Options
/// Include a `data-sourcepos` attribute on all block elements
/// Include a `data-sourcepos` attribute on all block elements.
public static let sourcePos = DownOptions(rawValue: CMARK_OPT_SOURCEPOS)
/// Render `softbreak` elements as hard line breaks.
public static let hardBreaks = DownOptions(rawValue: CMARK_OPT_HARDBREAKS)
/// Suppress raw HTML and unsafe links (`javascript:`, `vbscript:`,
@@ -32,8 +41,9 @@ public struct DownOptions: OptionSet {
///
/// Note: this is the default option as of cmark v0.29.0. Use `unsafe`
/// to disable this behavior.
public static let safe = DownOptions(rawValue: CMARK_OPT_SAFE)
/// Render raw HTML and unsafe links (`javascript:`, `vbscript:`,
/// `file:`, and `data:`, except for `image/png`, `image/gif`,
/// `image/jpeg`, or `image/webp` mime types). By default,
@@ -41,23 +51,28 @@ public struct DownOptions: OptionSet {
/// links are replaced by empty strings.
///
/// Note: `safe` is the default as of cmark v0.29.0
public static let unsafe = DownOptions(rawValue: CMARK_OPT_UNSAFE)
// MARK: - Parsing Options
/// Normalize tree by consolidating adjacent text nodes.
public static let normalize = DownOptions(rawValue: CMARK_OPT_NORMALIZE)
/// Validate UTF-8 in the input before parsing, replacing illegal
/// sequences with the replacement character U+FFFD.
public static let validateUTF8 = DownOptions(rawValue: CMARK_OPT_VALIDATE_UTF8)
/// Convert straight quotes to curly, --- to em dashes, -- to en dashes.
public static let smart = DownOptions(rawValue: CMARK_OPT_SMART)
// MARK: - Combo Options
/// Combines 'unsafe' and 'smart' to render raw HTML and produce smart typography.
public static let smartUnsafe = DownOptions(rawValue: CMARK_OPT_SMART + CMARK_OPT_UNSAFE)
}
@@ -9,18 +9,25 @@
#if !os(Linux)
#if os(macOS)
import AppKit
#else
import UIKit
#endif
import AppKit
#else
import UIKit
#endif
extension NSAttributedString {
/// Instantiates an attributed string with the given HTML string
///
/// - Parameter htmlString: An HTML string
/// - Throws: `HTMLDataConversionError` or an instantiation error
/// - Parameters:
/// - htmlString: An HTML string.
///
/// - Throws:
/// `HTMLDataConversionError` or an instantiation error.
convenience init(htmlString: String) throws {
guard let data = htmlString.data(using: String.Encoding.utf8) else {
throw DownErrors.htmlDataConversionError
@@ -30,8 +37,10 @@ extension NSAttributedString {
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: NSNumber(value: String.Encoding.utf8.rawValue)
]
try self.init(data: data, options: options, documentAttributes: nil)
}
}
#endif // !os(Linux)
+9 -4
View File
@@ -11,11 +11,16 @@ import libcmark
extension String {
/// Generates an HTML string from the contents of the string (self), which should contain CommonMark Markdown
/// Generates an HTML string from the contents of the string (self), which should contain CommonMark Markdown.
///
/// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: HTML string
/// - Throws: `DownErrors` depending on the scenario
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - Returns:
/// An HTML string.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toHTML(_ options: DownOptions = .default) throws -> String {
let ast = try DownASTRenderer.stringToAST(self, options: options)
let html = try DownHTMLRenderer.astToHTML(ast, options: options)
+41 -17
View File
@@ -10,24 +10,39 @@ import Foundation
import libcmark
public protocol DownASTRenderable: DownRenderable {
func toAST(_ options: DownOptions) throws -> UnsafeMutablePointer<cmark_node>
func toAST(_ options: DownOptions) throws -> CMarkNode
}
extension DownASTRenderable {
/// Generates an abstract syntax tree from the `markdownString` property
/// Generates an abstract syntax tree from the `markdownString` property.
///
/// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: An abstract syntax tree representation of the Markdown input
/// - Throws: `MarkdownToASTError` if conversion fails
public func toAST(_ options: DownOptions = .default) throws -> UnsafeMutablePointer<cmark_node> {
/// - Parametera:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
///
/// - Returns:
/// An abstract syntax tree representation of the Markdown input.
///
/// - Throws:
/// `MarkdownToASTError` if conversion fails.
public func toAST(_ options: DownOptions = .default) throws -> CMarkNode {
return try DownASTRenderer.stringToAST(markdownString, options: options)
}
/// Parses the `markdownString` property into an abstract syntax tree and returns the root `Document` node.
///
/// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: The root Document node for the abstract syntax tree representation of the Markdown input
/// - Throws: `MarkdownToASTError` if conversion fails
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
///
/// - Returns:
/// The root Document node for the abstract syntax tree representation of the Markdown input.
///
/// - Throws:
/// `MarkdownToASTError` if conversion fails.
public func toDocument(_ options: DownOptions = .default) throws -> Document {
let tree = try toAST(options)
@@ -37,20 +52,27 @@ extension DownASTRenderable {
return Document(cmarkNode: tree)
}
}
public struct DownASTRenderer {
/// Generates an abstract syntax tree from the given CommonMark Markdown string
/// Generates an abstract syntax tree from the given CommonMark Markdown string.
///
/// **Important:** It is the caller's responsibility to call `cmark_node_free(ast)` on the returned value
/// **Important:** It is the caller's responsibility to call `cmark_node_free(ast)` on the returned value.
///
/// - Parameters:
/// - string: A string containing CommonMark Markdown
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: An abstract syntax tree representation of the Markdown input
/// - Throws: `MarkdownToASTError` if conversion fails
public static func stringToAST(_ string: String, options: DownOptions = .default) throws -> UnsafeMutablePointer<cmark_node> {
var tree: UnsafeMutablePointer<cmark_node>?
/// - string: A string containing CommonMark Markdown.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
///
/// - Returns:
/// An abstract syntax tree representation of the Markdown input.
///
/// - Throws:
/// `MarkdownToASTError` if conversion fails.
public static func stringToAST(_ string: String, options: DownOptions = .default) throws -> CMarkNode {
var tree: CMarkNode?
string.withCString {
let stringLength = Int(strlen($0))
tree = cmark_parse_document($0, stringLength, options.rawValue)
@@ -59,6 +81,8 @@ public struct DownASTRenderer {
guard let ast = tree else {
throw DownErrors.markdownToASTError
}
return ast
}
}
@@ -7,16 +7,20 @@
//
#if !os(Linux)
import Foundation
import libcmark
public protocol DownAttributedStringRenderable: DownHTMLRenderable, DownASTRenderable {
func toAttributedString(_ options: DownOptions, stylesheet: String?) throws -> NSAttributedString
func toAttributedString(_ options: DownOptions, styler: Styler) throws -> NSAttributedString
}
extension DownAttributedStringRenderable {
/// Generates an `NSAttributedString` from the `markdownString` property
/// Generates an `NSAttributedString` from the `markdownString` property.
///
/// **Note:** The attributed string is constructed and rendered via WebKit from html generated from the
/// abstract syntax tree. This process is not background safe and must be executed on the main
@@ -24,31 +28,46 @@ extension DownAttributedStringRenderable {
/// use the `toAttributedString(options: styler:)` method below.
///
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - stylesheet: a `String` to use as the CSS stylesheet when rendering, defaulting to a style that uses the `NSAttributedString` default font
/// - Returns: An `NSAttributedString`
/// - Throws: `DownErrors` depending on the scenario
public func toAttributedString(_ options: DownOptions = .default, stylesheet: String? = nil) throws -> NSAttributedString {
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - stylesheet: a `String` to use as the CSS stylesheet when rendering, defaulting
/// to a style that uses the `NSAttributedString` default font.
///
/// - Returns:
/// An `NSAttributedString`.
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toAttributedString(_ options: DownOptions = .default,
stylesheet: String? = nil) throws -> NSAttributedString {
let html = try self.toHTML(options)
let defaultStylesheet = "* {font-family: Helvetica } code, pre { font-family: Menlo }"
return try NSAttributedString(htmlString: "<style>" + (stylesheet ?? defaultStylesheet) + "</style>" + html)
}
/// Generates an `NSAttributedString` from the `markdownString` property
/// Generates an `NSAttributedString` from the `markdownString` property.
///
/// **Note:** The attributed string is constructed directly by traversing the abstract syntax tree. It is
/// much faster than the `toAttributedString(options: stylesheet)` method and it can be also be
/// rendered in a background thread.
///
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering
/// - styler: a class/struct conforming to `Styler` to use when rendering the various elements of the attributed string
/// - Returns: An `NSAttributedString`
/// - Throws: `DownErrors` depending on the scenario
/// - options: `DownOptions` to modify parsing or rendering.
/// - styler: a class/struct conforming to `Styler` to use when rendering the various
/// elements of the attributed string
///
/// - Returns:
/// An `NSAttributedString`.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toAttributedString(_ options: DownOptions = .default, styler: Styler) throws -> NSAttributedString {
let document = try self.toDocument(options)
let visitor = AttributedStringVisitor(styler: styler, options: options)
return document.accept(visitor)
}
}
#endif // !os(Linux)
@@ -10,48 +10,68 @@ import Foundation
import libcmark
public protocol DownCommonMarkRenderable: DownRenderable {
func toCommonMark(_ options: DownOptions, width: Int32) throws -> String
}
extension DownCommonMarkRenderable {
/// Generates a CommonMark Markdown string from the `markdownString` property
/// Generates a CommonMark Markdown string from the `markdownString` property.
///
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - width: The width to break on, defaulting to 0
/// - Returns: CommonMark Markdown string
/// - Throws: `DownErrors` depending on the scenario
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - width: The width to break on, defaulting to 0.
///
/// - Returns:
/// A CommonMark Markdown string.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toCommonMark(_ options: DownOptions = .default, width: Int32 = 0) throws -> String {
let ast = try DownASTRenderer.stringToAST(markdownString, options: options)
let commonMark = try DownCommonMarkRenderer.astToCommonMark(ast, options: options, width: width)
cmark_node_free(ast)
return commonMark
}
}
public struct DownCommonMarkRenderer {
/// Generates a CommonMark Markdown string from the given abstract syntax tree
/// Generates a CommonMark Markdown string from the given abstract syntax tree.
///
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns.
///
/// - Parameters:
/// - ast: The `cmark_node` representing the abstract syntax tree
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - width: The width to break on, defaulting to 0
/// - Returns: CommonMark Markdown string
/// - Throws: `ASTRenderingError` if the AST could not be converted
public static func astToCommonMark(_ ast: UnsafeMutablePointer<cmark_node>,
/// - ast: The `cmark_node` representing the abstract syntax tree.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - width: The width to break on, defaulting to 0.
///
/// - Returns:
/// A CommonMark Markdown string.
///
/// - Throws:
/// `ASTRenderingError` if the AST could not be converted.
public static func astToCommonMark(_ ast: CMarkNode,
options: DownOptions = .default,
width: Int32 = 0) throws -> String {
guard let cCommonMarkString = cmark_render_commonmark(ast, options.rawValue, width) else {
throw DownErrors.astRenderingError
}
defer { free(cCommonMarkString) }
defer {
free(cCommonMarkString)
}
guard let commonMarkString = String(cString: cCommonMarkString, encoding: String.Encoding.utf8) else {
throw DownErrors.astRenderingError
}
return commonMarkString
}
}
@@ -10,48 +10,68 @@ import Foundation
import libcmark
public protocol DownGroffRenderable: DownRenderable {
func toGroff(_ options: DownOptions, width: Int32) throws -> String
}
extension DownGroffRenderable {
/// Generates a groff man string from the `markdownString` property
/// Generates a groff man string from the `markdownString` property.
///
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - width: The width to break on, defaulting to 0
/// - Returns: groff man string
/// - Throws: `DownErrors` depending on the scenario
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - width: The width to break on, defaulting to 0.
///
/// - Returns:
/// A groff man string.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toGroff(_ options: DownOptions = .default, width: Int32 = 0) throws -> String {
let ast = try DownASTRenderer.stringToAST(markdownString, options: options)
let groff = try DownGroffRenderer.astToGroff(ast, options: options, width: width)
cmark_node_free(ast)
return groff
}
}
public struct DownGroffRenderer {
/// Generates a groff man string from the given abstract syntax tree
/// Generates a groff man string from the given abstract syntax tree.
///
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns.
///
/// - Parameters:
/// - ast: The `cmark_node` representing the abstract syntax tree
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - width: The width to break on, defaulting to 0
/// - Returns: groff man string
/// - Throws: `ASTRenderingError` if the AST could not be converted
public static func astToGroff(_ ast: UnsafeMutablePointer<cmark_node>,
/// - ast: The `cmark_node` representing the abstract syntax tree.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - width: The width to break on, defaulting to 0.
///
/// - Returns:
/// A groff man string.
///
/// - Throws:
/// `ASTRenderingError` if the AST could not be converted.
public static func astToGroff(_ ast: CMarkNode,
options: DownOptions = .default,
width: Int32 = 0) throws -> String {
guard let cGroffString = cmark_render_man(ast, options.rawValue, width) else {
throw DownErrors.astRenderingError
}
defer { free(cGroffString) }
defer {
free(cGroffString)
}
guard let groffString = String(cString: cGroffString, encoding: String.Encoding.utf8) else {
throw DownErrors.astRenderingError
}
return groffString
}
}
+33 -13
View File
@@ -10,40 +10,60 @@ import Foundation
import libcmark
public protocol DownHTMLRenderable: DownRenderable {
func toHTML(_ options: DownOptions) throws -> String
}
extension DownHTMLRenderable {
/// Generates an HTML string from the `markdownString` property
/// Generates an HTML string from the `markdownString` property.
///
/// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: HTML string
/// - Throws: `DownErrors` depending on the scenario
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
///
/// - Returns:
/// An HTML string.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toHTML(_ options: DownOptions = .default) throws -> String {
return try markdownString.toHTML(options)
}
}
public struct DownHTMLRenderer {
/// Generates an HTML string from the given abstract syntax tree
/// Generates an HTML string from the given abstract syntax tree.
///
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns.
///
/// - Parameters:
/// - ast: The `cmark_node` representing the abstract syntax tree
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: HTML string
/// - Throws: `ASTRenderingError` if the AST could not be converted
public static func astToHTML(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default) throws -> String {
/// - ast: The `cmark_node` representing the abstract syntax tree.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
///
/// - Returns:
/// An HTML string.
///
/// - Throws:
/// `ASTRenderingError` if the AST could not be converted.
public static func astToHTML(_ ast: CMarkNode, options: DownOptions = .default) throws -> String {
guard let cHTMLString = cmark_render_html(ast, options.rawValue) else {
throw DownErrors.astRenderingError
}
defer { free(cHTMLString) }
defer {
free(cHTMLString)
}
guard let htmlString = String(cString: cHTMLString, encoding: String.Encoding.utf8) else {
throw DownErrors.astRenderingError
}
return htmlString
}
}
@@ -10,48 +10,68 @@ import Foundation
import libcmark
public protocol DownLaTeXRenderable: DownRenderable {
func toLaTeX(_ options: DownOptions, width: Int32) throws -> String
}
extension DownLaTeXRenderable {
/// Generates a LaTeX string from the `markdownString` property
/// Generates a LaTeX string from the `markdownString` property.
///
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - width: The width to break on, defaulting to 0
/// - Returns: LaTeX string
/// - Throws: `DownErrors` depending on the scenario
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - width: The width to break on, defaulting to 0.
///
/// - Returns:
/// A LaTeX string.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toLaTeX(_ options: DownOptions = .default, width: Int32 = 0) throws -> String {
let ast = try DownASTRenderer.stringToAST(markdownString, options: options)
let latex = try DownLaTeXRenderer.astToLaTeX(ast, options: options, width: width)
cmark_node_free(ast)
return latex
}
}
public struct DownLaTeXRenderer {
/// Generates a LaTeX string from the given abstract syntax tree
/// Generates a LaTeX string from the given abstract syntax tree.
///
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns.
///
/// - Parameters:
/// - ast: The `cmark_node` representing the abstract syntax tree
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - width: The width to break on, defaulting to 0
/// - Returns: LaTeX string
/// - Throws: `ASTRenderingError` if the AST could not be converted
public static func astToLaTeX(_ ast: UnsafeMutablePointer<cmark_node>,
/// - ast: The `cmark_node` representing the abstract syntax tree.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - width: The width to break on, defaulting to 0.
///
/// - Returns:
/// A LaTeX string.
///
/// - Throws:
/// `ASTRenderingError` if the AST could not be converted.
public static func astToLaTeX(_ ast: CMarkNode,
options: DownOptions = .default,
width: Int32 = 0) throws -> String {
guard let cLatexString = cmark_render_latex(ast, options.rawValue, width) else {
throw DownErrors.astRenderingError
}
defer { free(cLatexString) }
defer {
free(cLatexString)
}
guard let latexString = String(cString: cLatexString, encoding: String.Encoding.utf8) else {
throw DownErrors.astRenderingError
}
return latexString
}
}
+4 -1
View File
@@ -9,6 +9,9 @@
import Foundation
public protocol DownRenderable {
/// A string containing CommonMark Markdown
/// A string containing CommonMark Markdown.
var markdownString: String { get set }
}
+32 -12
View File
@@ -10,43 +10,63 @@ import Foundation
import libcmark
public protocol DownXMLRenderable: DownRenderable {
func toXML(_ options: DownOptions) throws -> String
}
extension DownXMLRenderable {
/// Generates an XML string from the `markdownString` property
/// Generates an XML string from the `markdownString` property.
///
/// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: XML string
/// - Throws: `DownErrors` depending on the scenario
/// - Parameters:
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
///
/// - Returns:
/// An XML string.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func toXML(_ options: DownOptions = .default) throws -> String {
let ast = try DownASTRenderer.stringToAST(markdownString, options: options)
let xml = try DownXMLRenderer.astToXML(ast, options: options)
cmark_node_free(ast)
return xml
}
}
public struct DownXMLRenderer {
/// Generates an XML string from the given abstract syntax tree
///
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns
/// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns.
///
/// - Parameters:
/// - ast: The `cmark_node` representing the abstract syntax tree
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - Returns: XML string
/// - Throws: `ASTRenderingError` if the AST could not be converted
public static func astToXML(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default) throws -> String {
/// - ast: The `cmark_node` representing the abstract syntax tree.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
///
/// - Returns:
/// An XML string.
///
/// - Throws:
/// `ASTRenderingError` if the AST could not be converted.
public static func astToXML(_ ast: CMarkNode, options: DownOptions = .default) throws -> String {
guard let cXMLString = cmark_render_xml(ast, options.rawValue) else {
throw DownErrors.astRenderingError
}
defer { free(cXMLString) }
defer {
free(cXMLString)
}
guard let xmlString = String(cString: cXMLString, encoding: String.Encoding.utf8) else {
throw DownErrors.astRenderingError
}
return xmlString
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ extension Foundation.Bundle {
Bundle(for: BundleFinder.self).resourceURL,
// For command-line tools.
Bundle.main.bundleURL,
Bundle.main.bundleURL
]
for candidate in candidates {
+89 -45
View File
@@ -7,30 +7,48 @@
//
#if !os(Linux)
#if os(tvOS) || os(watchOS)
// Sorry, not available for tvOS nor watchOS
// Sorry, not available for tvOS nor watchOS
#else
import WebKit
// MARK: - Public API
public typealias DownViewClosure = () -> ()
public typealias DownViewClosure = () -> Void
open class DownView: WKWebView {
/// Initializes a web view with the results of rendering a CommonMark Markdown string
// MARK: - Life cycle
/// Initializes a web view with the results of rendering a CommonMark Markdown string.
///
/// - Parameters:
/// - frame: The frame size of the web view
/// - markdownString: A string containing CommonMark Markdown
/// - openLinksInBrowser: Whether or not to open links using an external browser
/// - templateBundle: Optional custom template bundle. Leaving this as `nil` will use the bundle included with Down.
/// - configuration: Optional custom web view configuration.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully
/// - writableBundle: Whether or not the bundle folder is writable.
/// - Throws: `DownErrors` depending on the scenario
public init(frame: CGRect, markdownString: String, openLinksInBrowser: Bool = true, templateBundle: Bundle? = nil, writableBundle: Bool = false, configuration: WKWebViewConfiguration? = nil, options: DownOptions = .default, didLoadSuccessfully: DownViewClosure? = nil) throws {
/// - frame: The frame size of the web view
/// - markdownString: A string containing CommonMark Markdown
/// - openLinksInBrowser: Whether or not to open links using an external browser
/// - templateBundle: Optional custom template bundle. Leaving this as `nil` will use the bundle included
/// with Down.
/// - configuration: Optional custom web view configuration.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully
/// - writableBundle: Whether or not the bundle folder is writable.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public init(frame: CGRect,
markdownString: String,
openLinksInBrowser: Bool = true,
templateBundle: Bundle? = nil,
writableBundle: Bool = false,
configuration: WKWebViewConfiguration? = nil,
options: DownOptions = .default,
didLoadSuccessfully: DownViewClosure? = nil) throws {
self.options = options
self.didLoadSuccessfully = didLoadSuccessfully
self.writableBundle = writableBundle
@@ -46,7 +64,7 @@ open class DownView: WKWebView {
super.init(frame: frame, configuration: configuration ?? WKWebViewConfiguration())
#if os(macOS)
setupMacEnvironment()
setupMacEnvironment()
#endif
if openLinksInBrowser || didLoadSuccessfully != nil { navigationDelegate = self }
@@ -62,22 +80,29 @@ open class DownView: WKWebView {
clearTemporaryDirectory()
}
#endif
// MARK: - API
/// Renders the given CommonMark Markdown string into HTML and updates the DownView while keeping the style intact
/// Renders the given CommonMark Markdown string into HTML and updates the DownView while keeping the style intact.
///
/// - Parameters:
/// - markdownString: A string containing CommonMark Markdown
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`
/// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully
/// - Throws: `DownErrors` depending on the scenario
public func update(markdownString: String, options: DownOptions? = nil, didLoadSuccessfully: DownViewClosure? = nil) throws {
/// - markdownString: A string containing CommonMark Markdown.
/// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`.
/// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully.
///
/// - Throws:
/// `DownErrors` depending on the scenario.
public func update(markdownString: String,
options: DownOptions? = nil,
didLoadSuccessfully: DownViewClosure? = nil) throws {
// Note: As the init method sets this initially, we only overwrite them if
// a non-nil value is passed in
// a non-nil value is passed in.
if let options = options {
self.options = options
}
if let didLoadSuccessfully = didLoadSuccessfully {
self.didLoadSuccessfully = didLoadSuccessfully
}
@@ -96,15 +121,11 @@ open class DownView: WKWebView {
}()
#if os(macOS)
private lazy var temporaryDirectoryURL: URL = {
return try! FileManager.default.url(for: .itemReplacementDirectory,
in: .userDomainMask,
appropriateFor: URL(fileURLWithPath: NSTemporaryDirectory()),
create: true).appendingPathComponent("Down", isDirectory: true)
}()
private var temporaryDirectoryURL: URL?
#endif
private var didLoadSuccessfully: DownViewClosure?
}
// MARK: - Private API
@@ -116,15 +137,15 @@ private extension DownView {
let pageHTMLString = try htmlFromTemplate(htmlString)
#if os(iOS)
if writableBundle {
let newIndexUrl = try writeTempIndexFile(pageHTMLString: pageHTMLString)
loadFileURL(newIndexUrl, allowingReadAccessTo: newIndexUrl.deletingLastPathComponent())
} else {
loadHTMLString(pageHTMLString, baseURL: baseURL)
}
if writableBundle {
let newIndexUrl = try writeTempIndexFile(pageHTMLString: pageHTMLString)
loadFileURL(newIndexUrl, allowingReadAccessTo: newIndexUrl.deletingLastPathComponent())
} else {
loadHTMLString(pageHTMLString, baseURL: baseURL)
}
#elseif os(macOS)
let indexURL = try createTemporaryBundle(pageHTMLString: pageHTMLString)
loadFileURL(indexURL, allowingReadAccessTo: indexURL.deletingLastPathComponent())
let indexURL = try createTemporaryBundle(pageHTMLString: pageHTMLString)
loadFileURL(indexURL, allowingReadAccessTo: indexURL.deletingLastPathComponent())
#endif
}
@@ -143,8 +164,19 @@ private extension DownView {
#if os(macOS)
func createTemporaryBundle(pageHTMLString: String) throws -> URL {
guard let bundleResourceURL = bundle.resourceURL
else { throw DownErrors.nonStandardBundleFormatError }
guard let bundleResourceURL = bundle.resourceURL else {
throw DownErrors.nonStandardBundleFormatError
}
let fileManager = FileManager.default
let temporaryDirectoryURL = try fileManager.url(for: .itemReplacementDirectory,
in: .userDomainMask,
appropriateFor: URL(fileURLWithPath: NSTemporaryDirectory()),
create: true).appendingPathComponent("Down", isDirectory: true)
self.temporaryDirectoryURL = temporaryDirectoryURL
let indexURL = temporaryDirectoryURL.appendingPathComponent("index.html", isDirectory: false)
// If updating markdown contents, no need to re-copy bundle.
@@ -168,7 +200,9 @@ private extension DownView {
@objc
func clearTemporaryDirectory() {
try? FileManager.default.removeItem(at: temporaryDirectoryURL)
if let temporaryDirectoryURL = temporaryDirectoryURL {
try? FileManager.default.removeItem(at: temporaryDirectoryURL)
}
}
#endif
@@ -177,11 +211,18 @@ private extension DownView {
// MARK: - WKNavigationDelegate
extension DownView: WKNavigationDelegate {
public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
public func webView(_ webView: WKWebView,
decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
decisionHandler(.allow)
}
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
public func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else { return decisionHandler(.allow) }
switch navigationAction.navigationType {
@@ -208,18 +249,21 @@ extension DownView: WKNavigationDelegate {
NSWorkspace.shared.open(url)
#endif
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
didLoadSuccessfully?()
}
}
private extension WKNavigationDelegate {
/// A wrapper for `UIApplication.shared.openURL` so that an empty default
/// implementation is available in app extensions
func openURL(url: URL) {}
}
#endif
#endif // !os(Linux)
@@ -1,7 +1,7 @@
SUPPORTED_PLATFORMS = macosx iphonesimulator iphoneos appletvos appletvsimulator
VALID_ARCHS[sdk=macosx*] = i386 x86_64 arm64
VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s
VALID_ARCHS[sdk=iphonesimulator*] = i386 x86_64
VALID_ARCHS[sdk=iphonesimulator*] = i386 x86_64 arm64
VALID_ARCHS[sdk=appletv*] = arm64
VALID_ARCHS[sdk=appletvsimulator*] = x86_64
@@ -11,9 +11,9 @@ import XCTest
class ListItemPrefixGeneratorTests: XCTestCase {
func testNumberPrefixGeneration() {
func testNumberStaticPrefixGeneration() {
// Given
let sut = ListItemPrefixGenerator(listType: .ordered(start: 3), numberOfItems: 3)
let sut = StaticListItemPrefixGenerator(listType: .ordered(start: 3), numberOfItems: 3, nestDepth: 1)
// Then
XCTAssertEqual("3.", sut.next())
@@ -22,9 +22,9 @@ class ListItemPrefixGeneratorTests: XCTestCase {
XCTAssertNil(sut.next())
}
func testBulletPrefixGeneration() {
func testBulletStaticPrefixGeneration() {
// Given
let sut = ListItemPrefixGenerator(listType: .bullet, numberOfItems: 3)
let sut = StaticListItemPrefixGenerator(listType: .bullet, numberOfItems: 3, nestDepth: 1)
// Then
XCTAssertEqual("", sut.next())
@@ -32,4 +32,5 @@ class ListItemPrefixGeneratorTests: XCTestCase {
XCTAssertEqual("", sut.next())
XCTAssertNil(sut.next())
}
}
+4 -2
View File
@@ -30,6 +30,7 @@ class NodeTests: XCTestCase {
// Then
XCTAssertEqual(sut.listNestDepthResults, [0, 1, 2])
}
}
// MARK: - Helpers
@@ -41,11 +42,11 @@ extension NodeTests {
let document = try Down(markdownString: markdown).toDocument()
document.accept(visitor)
} catch {
XCTFail()
XCTFail("Failed to generate document.")
}
}
}
}
private class NodeVisitor: DebugVisitor {
@@ -55,4 +56,5 @@ private class NodeVisitor: DebugVisitor {
listNestDepthResults.append(node.nestDepth)
return super.visit(list: node)
}
}
+40 -38
View File
@@ -11,18 +11,18 @@ import SnapshotTesting
class VisitorTests: XCTestCase {
func result(for markdown: String) -> String {
func result(for markdown: String) throws -> String {
let down = Down(markdownString: markdown)
return try! down.toAttributedString(styler: EmptyStyler()).string
return try down.toAttributedString(styler: EmptyStyler()).string
}
func debugResult(for markdown: String) -> String {
func debugResult(for markdown: String) throws -> String {
let down = Down(markdownString: markdown)
let document = try! down.toDocument()
let document = try down.toDocument()
return document.accept(DebugVisitor())
}
func testBlockQuote() {
func testBlockQuote() throws {
// Given
let markdown = """
Text text.
@@ -35,11 +35,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testList() {
func testList() throws {
// Given
let markdown = """
Text text.
@@ -54,11 +54,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testCodeBlock() {
func testCodeBlock() throws {
// Given
let markdown = """
Text text.
@@ -72,11 +72,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testHtmlBlock() {
func testHtmlBlock() throws {
// Given
let markdown = """
Text text.
@@ -89,11 +89,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testParagraph() {
func testParagraph() throws {
// Given
let markdown = """
Text text.
@@ -104,11 +104,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testHeading() {
func testHeading() throws {
// Given
let markdown = """
Text text.
@@ -119,11 +119,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testThematicBreak() {
func testThematicBreak() throws {
// Given
let markdown = """
Text text.
@@ -134,11 +134,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testSoftBreak() {
func testSoftBreak() throws {
// Given
let markdown = """
Text text
@@ -146,11 +146,11 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testLineBreak() {
func testLineBreak() throws {
// Given
let markdown = """
Text text.\\
@@ -158,35 +158,36 @@ class VisitorTests: XCTestCase {
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testInline() {
func testInline() throws {
// Given
let markdown = """
Text **strong _emphasis `code` <html>_**
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
func testLink() {
func testLink() throws {
// Given
let markdown = """
Text [link](www.example.com) text ![image](www.example.com)
"""
// Then
assertSnapshot(matching: result(for: markdown), as: .lines)
assertSnapshot(matching: debugResult(for: markdown), as: .lines)
assertSnapshot(matching: try result(for: markdown), as: .lines)
assertSnapshot(matching: try debugResult(for: markdown), as: .lines)
}
}
private class EmptyStyler: Styler {
var listPrefixAttributes: [NSAttributedString.Key : Any] = [:]
var listPrefixAttributes: [NSAttributedString.Key: Any] = [:]
func style(document str: NSMutableAttributedString) {}
func style(blockQuote str: NSMutableAttributedString, nestDepth: Int) {}
func style(list str: NSMutableAttributedString, nestDepth: Int) {}
@@ -208,4 +209,5 @@ private class EmptyStyler: Styler {
func style(strong str: NSMutableAttributedString) {}
func style(link str: NSMutableAttributedString, title: String?, url: String?) {}
func style(image str: NSMutableAttributedString, title: String?, url: String?) {}
}
@@ -1,7 +1,7 @@
Document
↳ Paragraph
↳ Text - Text text.
↳ List - type: Ordered (start: 3), isTight: true
↳ List - type: Ordered (start: 3), isTight: true, delimiter: period
↳ Item
↳ Paragraph
↳ Text - One
+20 -25
View File
@@ -7,45 +7,40 @@
//
import XCTest
import SnapshotTesting
@testable import Down
class BindingTests: XCTestCase {
let down = Down(markdownString: "## [Down](https://github.com/iwasrobbed/Down)")
let down = Down(markdownString: "## [Down](https://github.com/johnxnnguyen/Down)")
func testASTBindingsWork() {
let ast = try? down.toAST()
XCTAssertNotNil(ast)
func testASTBindingsWork() throws {
_ = try down.toAST()
}
func testHTMLBindingsWork() {
let html = try? down.toHTML()
XCTAssertNotNil(html)
XCTAssertTrue(html == "<h2><a href=\"https://github.com/iwasrobbed/Down\">Down</a></h2>\n")
func testHTMLBindingsWork() throws {
let html = try down.toHTML()
assertSnapshot(matching: html, as: .lines)
}
func testXMLBindingsWork() {
let xml = try? down.toXML()
XCTAssertNotNil(xml)
XCTAssertTrue(xml == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n<document xmlns=\"http://commonmark.org/xml/1.0\">\n <heading level=\"2\">\n <link destination=\"https://github.com/iwasrobbed/Down\" title=\"\">\n <text xml:space=\"preserve\">Down</text>\n </link>\n </heading>\n</document>\n")
func testXMLBindingsWork() throws {
let xml = try down.toXML()
assertSnapshot(matching: xml, as: .lines)
}
func testGroffBindingsWork() {
let man = try? down.toGroff()
XCTAssertNotNil(man)
XCTAssertTrue(man == ".SS\nDown (https://github.com/iwasrobbed/Down)\n")
func testGroffBindingsWork() throws {
let man = try down.toGroff()
assertSnapshot(matching: man, as: .lines)
}
func testLaTeXBindngsWork() {
let latex = try? down.toLaTeX()
XCTAssertNotNil(latex)
XCTAssertTrue(latex == "\\subsection{\\href{https://github.com/iwasrobbed/Down}{Down}}\n")
func testLaTeXBindngsWork() throws {
let latex = try down.toLaTeX()
assertSnapshot(matching: latex, as: .lines)
}
func testCommonMarkBindngsWork() {
let commonMark = try? down.toCommonMark()
XCTAssertNotNil(commonMark)
XCTAssertTrue(commonMark == "## [Down](https://github.com/iwasrobbed/Down)\n")
func testCommonMarkBindngsWork() throws {
let commonMark = try down.toCommonMark()
assertSnapshot(matching: commonMark, as: .lines)
}
}
+51 -27
View File
@@ -18,22 +18,26 @@ class DownViewTests: XCTestCase {
func testInstantiation() {
let expect1 = expectation(description: "DownView sets the html and validates the html is correct")
var downView: DownView?
downView = try? DownView(frame: .zero, markdownString: "## [Down](https://github.com/iwasrobbed/Down)", didLoadSuccessfully: {
downView = try? DownView(frame: .zero,
markdownString: "## [Down](https://github.com/iwasrobbed/Down)",
didLoadSuccessfully: {
self._pageContents(for: downView!) { htmlString in
XCTAssertTrue(htmlString!.contains("css/down.min.css"))
XCTAssertTrue(htmlString!.contains("https://github.com/iwasrobbed/Down"))
expect1.fulfill()
}
})
waitForExpectations(timeout: 10) { (error: Error?) in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testUpdatingMarkdown() {
let expect1 = expectation(description: "DownView sets the html and validates the html is correct")
var downView: DownView?
@@ -41,27 +45,27 @@ class DownViewTests: XCTestCase {
self._pageContents(for: downView!) { htmlString in
XCTAssertTrue(htmlString!.contains("css/down.min.css"))
XCTAssertTrue(htmlString!.contains("https://github.com/iwasrobbed/Down"))
expect1.fulfill()
}
}
waitForExpectations(timeout: 10) { (error: Error?) in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
}
let expect2 = expectation(description: "DownView sets the html and validates the html is correct")
try? downView?.update(markdownString: "## [Google](https://google.com)") {
try? downView?.update(markdownString: "## [Google](https://google.com)") {
self._pageContents(for: downView!) { htmlString in
XCTAssertTrue(htmlString!.contains("css/down.min.css"))
XCTAssertTrue(htmlString!.contains("https://google.com"))
expect2.fulfill()
}
}
waitForExpectations(timeout: 10) { (error: Error?) in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
@@ -80,7 +84,11 @@ class DownViewTests: XCTestCase {
}
var downView: DownView?
downView = try? DownView(frame: .zero, markdownString: "## [Down](https://github.com/iwasrobbed/Down)", templateBundle: templateBundle, didLoadSuccessfully: {
downView = try? DownView(frame: .zero,
markdownString: "## [Down](https://github.com/iwasrobbed/Down)",
templateBundle: templateBundle,
didLoadSuccessfully: {
self._pageContents(for: downView!) { htmlString in
XCTAssertTrue(htmlString!.contains("css/down.min.css"))
XCTAssertTrue(htmlString!.contains("https://github.com/iwasrobbed/Down"))
@@ -98,7 +106,9 @@ class DownViewTests: XCTestCase {
}
func testInstantiationWithCustomWritableTemplateBundle() {
let expect1 = expectation(description: "DownView accepts and loads custom bundle files from a user writable location")
let expect1 = expectation(
description: "DownView accepts and loads custom bundle files from a user writable location"
)
guard
let bundle = Bundle(for: type(of: self)).url(forResource: "TestDownView", withExtension: "bundle"),
@@ -108,13 +118,20 @@ class DownViewTests: XCTestCase {
return
}
let markdownString = """
```swift
let x = 1
```
"""
let markdown = """
```swift
let x = 1
```
"""
var downView: DownView?
downView = try? DownView(frame: .zero, markdownString: markdownString, templateBundle: templateBundle, writableBundle: true, didLoadSuccessfully: {
downView = try? DownView(frame: .zero,
markdownString: markdown,
templateBundle: templateBundle,
writableBundle: true,
didLoadSuccessfully: {
self._pageContents(for: downView!) { htmlString in
XCTAssertTrue(htmlString!.contains("css/down.min.css"))
XCTAssertTrue(htmlString!.contains("hljs-keyword"))
@@ -132,21 +149,21 @@ let x = 1
}
func testDownOptions() {
let markdownString = "## [Down](https://github.com/iwasrobbed/Down)\n\n<strong>I'm strong!</strong>"
let markdown = "## [Down](https://github.com/iwasrobbed/Down)\n\n<strong>I'm strong!</strong>"
let renderedHTML = "<strong>I'm strong!</strong>"
// Set this view to initially be HTML safe
let safeExpect = expectation(description: "DownView default init strips unsafe HTML")
let toggleSafeExpect = expectation(description: "DownView update to unsafe does not strip unsafe HTML")
var safeDownView: DownView?
safeDownView = try? DownView(frame: .zero, markdownString: markdownString, didLoadSuccessfully: {
safeDownView = try? DownView(frame: .zero, markdownString: markdown, didLoadSuccessfully: {
self._pageContents(for: safeDownView!) { htmlString in
XCTAssertTrue(safeDownView?.options == .default)
XCTAssertFalse(htmlString!.contains(renderedHTML))
safeExpect.fulfill()
// Then change it to HTML unsafe options and ensure it's changed
try? safeDownView?.update(markdownString: markdownString, options: .unsafe, didLoadSuccessfully: {
try? safeDownView?.update(markdownString: markdown, options: .unsafe, didLoadSuccessfully: {
XCTAssertTrue(safeDownView?.options == .unsafe)
self._pageContents(for: safeDownView!) { htmlString in
XCTAssertTrue(htmlString!.contains(renderedHTML))
@@ -160,14 +177,15 @@ let x = 1
let unsafeExpect = expectation(description: "DownView unsafe init does not strip unsafe HTML")
let toggleUnsafeExpect = expectation(description: "DownView update to safe strips unsafe HTML")
var unsafeDownView: DownView?
unsafeDownView = try? DownView(frame: .zero, markdownString: markdownString, options: .unsafe, didLoadSuccessfully: {
unsafeDownView = try? DownView(frame: .zero, markdownString: markdown, options: .unsafe, didLoadSuccessfully: {
self._pageContents(for: unsafeDownView!) { htmlString in
XCTAssertTrue(unsafeDownView?.options == .unsafe)
XCTAssertTrue(htmlString!.contains(renderedHTML))
unsafeExpect.fulfill()
// And then toggle it to be HTML safe and ensure it's changed
try? unsafeDownView?.update(markdownString: markdownString, options: .default, didLoadSuccessfully: {
try? unsafeDownView?.update(markdownString: markdown, options: .default, didLoadSuccessfully: {
XCTAssertTrue(unsafeDownView?.options == .default)
self._pageContents(for: unsafeDownView!) { htmlString in
XCTAssertFalse(htmlString!.contains(renderedHTML))
@@ -222,7 +240,11 @@ let x = 1
let configuration = WKWebViewConfiguration()
configuration.setURLSchemeHandler(mockURLSchemeHandler, forURLScheme: mockURLScheme)
downView = try? DownView(frame: .zero, markdownString: "[Link](\(mockURL.absoluteString))", openLinksInBrowser: true, configuration: configuration, didLoadSuccessfully: didLoadSuccessfully)
downView = try? DownView(frame: .zero,
markdownString: "[Link](\(mockURL.absoluteString))",
openLinksInBrowser: true,
configuration: configuration,
didLoadSuccessfully: didLoadSuccessfully)
waitForExpectations(timeout: 10) { (error: Error?) in
if let error = error {
@@ -230,15 +252,17 @@ let x = 1
}
}
}
}
private extension DownViewTests {
func _pageContents(for downView: DownView, completion: @escaping (_ htmlString: String?) -> ()) {
func _pageContents(for downView: DownView, completion: @escaping (_ htmlString: String?) -> Void) {
downView.evaluateJavaScript("document.documentElement.outerHTML.toString()") { (html: Any?, _) in
completion(html as? String)
}
}
}
#endif
@@ -12,7 +12,8 @@ import XCTest
class NSAttributedStringTests: XCTestCase {
func testAttributedStringBindingsWork() {
let attributedString = try? Down(markdownString: "## [Down](https://github.com/iwasrobbed/Down)").toAttributedString()
let markdown = "## [Down](https://github.com/johnxnguyen/Down)"
let attributedString = try? Down(markdownString: markdown).toAttributedString()
XCTAssertNotNil(attributedString)
XCTAssertTrue(attributedString!.string == "Down\n")
}
@@ -6,13 +6,14 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class BlockQuoteStyleTests: StylerTestSuite {
/// # Important
///
/// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
/// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
///
// # Important
//
// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
// MARK: - Alignment
@@ -204,5 +205,7 @@ class BlockQuoteStyleTests: StylerTestSuite {
// Then
assertStyle(for: markdown, width: .wide)
}
}
#endif
@@ -6,6 +6,8 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class CodeBlockStyleTests: StylerTestSuite {
/// # Important
@@ -50,4 +52,7 @@ class CodeBlockStyleTests: StylerTestSuite {
// Then
assertStyle(for: markdown, width: .wide)
}
}
#endif
@@ -6,6 +6,8 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class DownDebugLayoutManagerTests: StylerTestSuite {
/// # Important
@@ -40,3 +42,5 @@ class DownDebugLayoutManagerTests: StylerTestSuite {
assertStyle(for: markdown, width: .wide, showLineFragments: true)
}
}
#endif
@@ -6,13 +6,14 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class HeadingStyleTests: StylerTestSuite {
/// # Important
///
/// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
/// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
///
// # Important
//
// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
// MARK: - Heading Levels
@@ -103,4 +104,7 @@ class HeadingStyleTests: StylerTestSuite {
// Then
assertStyle(for: markdown, width: .wide)
}
}
#endif
@@ -9,7 +9,7 @@
import XCTest
@testable import Down
class CGPoint_TranslateTests: XCTestCase {
class CGPointTranslateTests: XCTestCase {
func testPointTranslation() {
// Given
@@ -21,4 +21,5 @@ class CGPoint_TranslateTests: XCTestCase {
// Then
XCTAssertEqual(CGPoint(x: 4, y: 6), result)
}
}
@@ -9,7 +9,7 @@
import XCTest
@testable import Down
class CGRect_HelpersTests: XCTestCase {
class CGRectHelpersTests: XCTestCase {
func testRectInitializationWithBoundaries() {
// When
@@ -29,4 +29,5 @@ class CGRect_HelpersTests: XCTestCase {
// Then
XCTAssertEqual(CGRect(x: 6, y: 8, width: 3, height: 4), result)
}
}
@@ -9,7 +9,7 @@
import XCTest
@testable import Down
class NSAttributedString_HelpersTests: XCTestCase {
class NSAttributedStringHelpersTests: XCTestCase {
let dummyKey = NSAttributedString.Key(rawValue: "key")
let dummyValue = "value"
@@ -91,8 +91,7 @@ class NSAttributedString_HelpersTests: XCTestCase {
XCTAssertEqual(result[1], NSRange(location: 19, length: 4)) // "you "
}
// MARK: - Missing Attribute Ranges
// MARK: - Missing Attribute Ranges
func testRangesMissingAttribute_None() {
// Given
@@ -165,7 +164,7 @@ class NSAttributedString_HelpersTests: XCTestCase {
func testParagraphRanges() {
// Given
let sut = NSAttributedString(string:"Hello\nhello\nworld")
let sut = NSAttributedString(string: "Hello\nhello\nworld")
// When
let result = sut.paragraphRanges()
@@ -180,7 +179,7 @@ class NSAttributedString_HelpersTests: XCTestCase {
func testParagraphRangesOfStringThatHasParagraphSeparators() {
// Given
let separator = "\u{2029}"
let sut = NSAttributedString(string:"Hello\(separator)hello\(separator)world")
let sut = NSAttributedString(string: "Hello\(separator)hello\(separator)world")
// When
let result = sut.paragraphRanges()
@@ -194,7 +193,7 @@ class NSAttributedString_HelpersTests: XCTestCase {
func testParagraphRangesOfStringWithLargeBreaks() {
// Given
let sut = NSAttributedString(string:"Hello\n\nhello\n\n\nworld")
let sut = NSAttributedString(string: "Hello\n\nhello\n\n\nworld")
// When
let result = sut.paragraphRanges()
@@ -206,28 +205,29 @@ class NSAttributedString_HelpersTests: XCTestCase {
XCTAssertEqual(result[2], NSRange(location: 15, length: 5)) // "world
}
// MARK: - Enumeration
// MARK: - Enumeration
func testEnumerationOfAttributes() {
// Given
let sut = NSMutableAttributedString()
sut.append(make("Hello ", attributed: true))
sut.append(make("world ", attributed: true))
sut.append(make("how do "))
sut.append(make("you ", attributed: true))
sut.append(make("do?"))
func testEnumerationOfAttributes() {
// Given
let sut = NSMutableAttributedString()
sut.append(make("Hello ", attributed: true))
sut.append(make("world ", attributed: true))
sut.append(make("how do "))
sut.append(make("you ", attributed: true))
sut.append(make("do?"))
// When
var result = [(String, NSRange)]()
sut.enumerateAttributes(for: dummyKey) { (attr: String, range) in
result.append((attr, range))
// When
var result = [(String, NSRange)]()
sut.enumerateAttributes(for: dummyKey) { (attr: String, range) in
result.append((attr, range))
}
// Then
XCTAssertEqual(result.count, 2)
XCTAssertEqual(result[0].0, "value")
XCTAssertEqual(result[0].1, NSRange(location: 0, length: 12)) // "Hello world "
XCTAssertEqual(result[1].0, "value")
XCTAssertEqual(result[1].1, NSRange(location: 19, length: 4)) // "you "
}
// Then
XCTAssertEqual(result.count, 2)
XCTAssertEqual(result[0].0, "value")
XCTAssertEqual(result[0].1, NSRange(location: 0, length: 12)) // "Hello world "
XCTAssertEqual(result[1].0, "value")
XCTAssertEqual(result[1].1, NSRange(location: 19, length: 4)) // "you "
}
}
@@ -9,7 +9,7 @@
import XCTest
@testable import Down
class NSMutableAttributedString_AttributesTests: XCTestCase {
class NSMutableAttributedStringAttributesTests: XCTestCase {
private let key1 = NSAttributedString.Key("dummyKey1")
private let key2 = NSAttributedString.Key("dummyKey2")
@@ -31,7 +31,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
attributeRanges = sut.ranges(of: key2)
XCTAssertEqual(attributeRanges, [sut.wholeRange])
XCTAssertTrue(value(for: key2, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
XCTAssertTrue(value(for: key2, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
}
func testAddingAttributes() {
@@ -44,7 +44,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
// Then
let attributeRanges = sut.ranges(of: key1)
XCTAssertEqual(attributeRanges, [sut.wholeRange])
XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
}
func testAddingAttribute() {
@@ -57,7 +57,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
// Then
let attributeRanges = sut.ranges(of: key1)
XCTAssertEqual(attributeRanges, [sut.wholeRange])
XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
}
func testRemovingAttribute() {
@@ -90,7 +90,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
let attributeRanges = sut.ranges(of: .foregroundColor)
XCTAssertEqual(attributeRanges.count, 1)
XCTAssertEqual(attributeRanges.first, NSRange(location: 0, length: 12))
XCTAssertTrue(value(for: .foregroundColor, inRange: attributeRanges.first!, isEqualTo: DownColor.yellow, sut: sut))
XCTAssertTrue(value(for: .foregroundColor, in: attributeRanges.first!, isEqualTo: DownColor.yellow, sut: sut))
}
func testUpdatingAttribute() {
@@ -105,7 +105,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
// Then
let attributeRanges = sut.ranges(of: key1)
XCTAssertEqual(attributeRanges, [sut.wholeRange])
XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue.uppercased(), sut: sut))
XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue.uppercased(), sut: sut))
}
func testUpdatingAttributeInRange() {
@@ -115,7 +115,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
let rangeOfSecondWord = NSRange(location: 6, length: 5)
// When
sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { (value: String) in
sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { _ in
"some new value"
}
@@ -123,7 +123,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
let attributeRanges = sut.ranges(of: key1)
XCTAssertEqual(attributeRanges.count, 2)
XCTAssertEqual(attributeRanges, [rangeOfFirstWord, rangeOfSecondWord])
XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: "some new value", sut: sut))
XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: "some new value", sut: sut))
}
func testUpdatingAttributeThatDidNotExistInRangeDoesNothing() {
@@ -132,7 +132,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
let rangeOfFirstWord = NSRange(location: 0, length: 6)
// When
sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { (value: String) in
sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { _ in
"some new value"
}
@@ -156,8 +156,8 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
let attributeRanges = sut.ranges(of: key1)
XCTAssertEqual(attributeRanges.count, 2)
XCTAssertEqual(attributeRanges, [rangeOfFirstWord, rangeOfSecondWord])
XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
XCTAssertTrue(value(for: key1, inRange: attributeRanges.last!, isEqualTo: "some new value", sut: sut))
XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
XCTAssertTrue(value(for: key1, in: attributeRanges.last!, isEqualTo: "some new value", sut: sut))
}
func testAdddingAttributeInMissingRangesDoesNothingIfNoMissingRanges() {
@@ -171,19 +171,25 @@ class NSMutableAttributedString_AttributesTests: XCTestCase {
let attributeRanges = sut.ranges(of: key1)
XCTAssertEqual(attributeRanges.count, 1)
XCTAssertEqual(attributeRanges.first!, sut.wholeRange)
XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut))
}
}
private extension NSMutableAttributedString_AttributesTests {
private extension NSMutableAttributedStringAttributesTests {
func countAttribute(_ name: NSAttributedString.Key, in str: NSAttributedString) -> Int {
str.ranges(of: name).count
}
func value<A: Equatable>(for name: NSAttributedString.Key, inRange: NSRange, isEqualTo aValue: A, sut: NSMutableAttributedString) -> Bool {
func value<A: Equatable>(for name: NSAttributedString.Key,
in range: NSRange,
isEqualTo aValue: A,
sut: NSMutableAttributedString) -> Bool {
var effectiveRange = NSRange()
let value = sut.attribute(name, at: inRange.location, effectiveRange: &effectiveRange) as? A
return value == aValue && effectiveRange == inRange
let value = sut.attribute(name, at: range.location, effectiveRange: &effectiveRange) as? A
return value == aValue && effectiveRange == range
}
}
@@ -6,13 +6,14 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class InlineStyleTests: StylerTestSuite {
/// # Important
///
/// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
/// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
///
// # Important
//
// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
// MARK: - Simple
@@ -91,4 +92,7 @@ class InlineStyleTests: StylerTestSuite {
// Then
assertStyle(for: markdown, width: .wide)
}
}
#endif
+6 -1
View File
@@ -6,6 +6,8 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class LinkStyleTests: StylerTestSuite {
/// # Important
@@ -17,7 +19,7 @@ class LinkStyleTests: StylerTestSuite {
func testThat_Link_IsStyled() {
// Given
let markdown = """
Praesent facilisis [pellentesque](www.example.com) ipsum at pulvinar. Sed consectetur augue vel mattis hendrerit.
Praesent facilisis [pellentesque](www.example.com) ipsum at pulvinar. Sed consectetur augue.
"""
// Then
@@ -33,4 +35,7 @@ class LinkStyleTests: StylerTestSuite {
// Then
assertStyle(for: markdown, width: .narrow)
}
}
#endif
@@ -6,13 +6,14 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class ListItemStyleTests: StylerTestSuite {
/// # Important
///
/// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
/// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
///
// # Important
//
// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise
// the comparison may fail. These tests were recorded on the **iPhone 12** simulator.
// MARK: - Prefix Alignment
@@ -262,4 +263,7 @@ class ListItemStyleTests: StylerTestSuite {
// Then
assertStyle(for: markdown, width: .narrow)
}
}
#endif
+34 -9
View File
@@ -6,6 +6,8 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
import XCTest
import SnapshotTesting
@testable import Down
@@ -18,7 +20,7 @@ class StylerTestSuite: XCTestCase {
var textContainerInset: UIEdgeInsets!
// MARK: - Lifecycle
// MARK: - Life cycle
override func setUp() {
super.setUp()
@@ -42,33 +44,52 @@ class StylerTestSuite: XCTestCase {
testName: String = #function,
line: UInt = #line) {
let view = self.view(for: markdown, width: width, configuration: configuration, showLineFragments: showLineFragments)
let maybeView = try? self.view(for: markdown,
width: width,
configuration: configuration,
showLineFragments: showLineFragments)
let failure = verifySnapshot(matching: view, as: .image, record: recording, file: file, testName: testName, line: line)
guard let view = maybeView else {
return XCTFail("Failed to generate markdown view.", file: file, line: line)
}
let failure = verifySnapshot(matching: view,
as: .image,
record: recording,
file: file,
testName: testName,
line: line)
guard let message = failure else { return }
XCTFail(message, file: file, line: line)
}
func view(for markdown: String, width: Width, configuration: DownStylerConfiguration?, showLineFragments: Bool = false) -> DownTextView {
func view(for markdown: String,
width: Width,
configuration: DownStylerConfiguration?,
showLineFragments: Bool = false) throws -> DownTextView {
// To make the snapshots the same size of the text content, we set a huge height then resize the view
// to the content size.
let frame = CGRect(x: 0, y: 0, width: width.rawValue, height: 5000)
let textView = showLineFragments ? DownDebugTextView(frame: frame) : DownTextView(frame: frame)
textView.textContainerInset = textContainerInset
textView.attributedText = attributedString(for: markdown, configuration: configuration)
textView.attributedText = try attributedString(for: markdown, configuration: configuration)
textView.layoutIfNeeded()
textView.resizeToContentSize()
return textView
}
private func attributedString(for markdown: String, configuration: DownStylerConfiguration?) -> NSAttributedString {
private func attributedString(for markdown: String,
configuration: DownStylerConfiguration?) throws -> NSAttributedString {
let down = Down(markdownString: markdown)
let styler = DownStyler(configuration: configuration ?? .testConfiguration)
return try! down.toAttributedString(styler: styler)
return try down.toAttributedString(styler: styler)
}
}
}
extension StylerTestSuite {
@@ -76,14 +97,15 @@ extension StylerTestSuite {
case narrow = 300
case wide = 600
}
}
}
private extension DownTextView {
func resizeToContentSize() {
frame = .init(origin: frame.origin, size: .init(width: contentSize.width, height: contentSize.height))
}
}
private extension DownStylerConfiguration {
@@ -160,4 +182,7 @@ private extension DownStylerConfiguration {
return configuration
}
}
#endif
@@ -6,6 +6,8 @@
// Copyright © 2016-2019 Down. All rights reserved.
//
#if os(iOS)
class ThematicBreakSyleTests: StylerTestSuite {
/// # Important
@@ -64,4 +66,7 @@ class ThematicBreakSyleTests: StylerTestSuite {
// Then
assertStyle(for: markdown, width: .wide, configuration: configuration)
}
}
#endif
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1 @@
## [Down](https://github.com/johnxnnguyen/Down)
@@ -0,0 +1,2 @@
.SS
Down (https://github.com/johnxnnguyen/Down)
@@ -0,0 +1 @@
<h2><a href="https://github.com/johnxnnguyen/Down">Down</a></h2>

Some files were not shown because too many files have changed in this diff Show More