Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/Ink/API/Modifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ public extension Modifier {
case lists
case paragraphs
case tables
case none
}
}
35 changes: 35 additions & 0 deletions Sources/Ink/Internal/GroupFragment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Ink
* Copyright (c) John Sundell 2019
* MIT license, see LICENSE file for details
*/

internal struct ParsedFragment {
var fragment: Fragment
var rawString: Substring
}

internal protocol GroupFragment: Fragment {
var fragments: [ParsedFragment] { get }
}

extension GroupFragment {
func html(usingURLs urls: NamedURLCollection, modifiers: ModifierCollection) -> String {
return fragments.reduce(into: "") { result, wrapper in
let html = wrapper.fragment.html(
usingURLs: urls,
rawString: wrapper.rawString,
applyingModifiers: modifiers
)

result.append(html)
}
}

func plainText() -> String {
return fragments.reduce(into: "") { result, wrapper in
let text = wrapper.fragment.plainText()
result.append(text)
}
}
}
76 changes: 62 additions & 14 deletions Sources/Ink/Internal/HTML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,55 @@
* MIT license, see LICENSE file for details
*/

internal struct HTML: Fragment {
internal struct HTML: GroupFragment {
var modifierTarget: Modifier.Target { .html }

private var string: Substring
let fragments: [ParsedFragment]

//private var string: Substring

static func read(using reader: inout Reader) throws -> HTML {
let startIndex = reader.currentIndex
var startIndex = reader.currentIndex
let rootElement = try reader.readHTMLElement()
var fragments: [ParsedFragment] = []

guard !rootElement.isSelfClosing else {
let html = reader.characters(in: startIndex..<reader.currentIndex)
return HTML(string: html)
return HTML(fragments: [ParsedFragment(fragment: RawHTML(string: html), rawString: html)])
}

var rootElementCount = 1
var possibleMarkdown = false

while !reader.didReachEnd {

// if this has been tagged as possible markdown and have found a markdown character
if possibleMarkdown,
let type = fragmentType(for: reader.currentCharacter, nextCharacter: reader.nextCharacter) {
// add raw html fragment
let html = reader.characters(in: startIndex..<reader.currentIndex).trimmingTrailingWhitespaces().trimmingLeadingWhitespaces()
fragments.append(ParsedFragment(fragment: RawHTML(string: html), rawString: html))

let fragment: ParsedFragment
do {
fragment = try makeFragment(using: type.readOrRewind, reader: &reader)
} catch {
fragment = makeFragment(using: Paragraph.read, reader: &reader)
}
fragments.append(fragment)
startIndex = reader.currentIndex
} else {
possibleMarkdown = false
}

guard !reader.didReachEnd else { break }

// if two newlines are found together set possibleMarkdown flag
if let previousCharacter = reader.previousCharacter {
if reader.currentCharacter.isNewline && previousCharacter.isNewline {
possibleMarkdown = true
}
}
guard reader.currentCharacter == "<" else {
reader.advanceIndex()
continue
Expand All @@ -47,19 +79,34 @@ internal struct HTML: Fragment {
}
}

let html = reader.characters(in: startIndex..<reader.currentIndex)
return HTML(string: html)
let html = reader.characters(in: startIndex..<reader.currentIndex).trimmingLeadingWhitespaces()
fragments.append(ParsedFragment(fragment: RawHTML(string: html), rawString: html))

return HTML(fragments: fragments)
}

func html(usingURLs urls: NamedURLCollection,
modifiers: ModifierCollection) -> String {
String(string)

static func makeFragment(using closure: (inout Reader) throws -> Fragment,
reader: inout Reader) rethrows -> ParsedFragment {
let startIndex = reader.currentIndex
let fragment = try closure(&reader)
let rawString = reader.characters(in: startIndex..<reader.currentIndex)
return ParsedFragment(fragment: fragment, rawString: rawString)
}

func plainText() -> String {
// Since we want to strip all HTML from plain text output,
// there is nothing to return here, just an empty string.
""
static func fragmentType(for character: Character,
nextCharacter: Character?) -> Fragment.Type? {
switch character {
case "#": return Heading.self
case "!": return Image.self
case "[": return Link.self
case ">": return Blockquote.self
case "`": return CodeBlock.self
case "-" where character == nextCharacter,
"*" where character == nextCharacter:
return HorizontalLine.self
case "-", "*", "+", \.isNumber: return List.self
default: return nil
}
}
}

Expand Down Expand Up @@ -89,3 +136,4 @@ private extension Reader {
throw Error()
}
}

24 changes: 24 additions & 0 deletions Sources/Ink/Internal/RawHTML.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Ink
* Copyright (c) John Sundell 2019
* MIT license, see LICENSE file for details
*/

struct RawHTML: Fragment {
var modifierTarget: Modifier.Target { .none }

var string: Substring

static func read(using reader: inout Reader) throws -> RawHTML {
return RawHTML(string:"")
}

func html(usingURLs urls: NamedURLCollection, modifiers: ModifierCollection) -> String {
return String(string)
}

func plainText() -> String {
return ""
}
}

136 changes: 135 additions & 1 deletion Tests/InkTests/HTMLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,130 @@ final class HTMLTests: XCTestCase {

XCTAssertEqual(html, "<p>Hello &amp; welcome to &lt;Ink&gt;</p>")
}

func testMarkdownHeadingInsideHTML() {
let html = MarkdownParser().html(from: """
<div>

## Heading

</div>
""")

XCTAssertEqual(html, "<div><h2>Heading</h2></div>")
}

func testMarkdownImageInsideHTML() {
let html = MarkdownParser().html(from: """
<div>

![test image](https://test.com/test.jpg)

</div>
""")

XCTAssertEqual(html, "<div><img src=\"https://test.com/test.jpg\" alt=\"test image\"/></div>")
}

func testMarkdownListInsideHTML() {
let html = MarkdownParser().html(from: """
<div>

- One
- Two
- Three

</div>
""")

XCTAssertEqual(html, "<div><ul><li>One</li><li>Two</li><li>Three</li></ul></div>")
}

func testMarkdownBeforeHTML() {
let html = MarkdownParser().html(from: """
<div>

# Heading1

<h2>Heading2</h2></div>
""")

XCTAssertEqual(html, "<div><h1>Heading1</h1><h2>Heading2</h2></div>")
}

func testMarkdownAfterHTML() {
let html = MarkdownParser().html(from: """
<div><h2>Heading2</h2>

# Heading1
</div>
""")

XCTAssertEqual(html, "<div><h2>Heading2</h2><h1>Heading1</h1></div>")
}


func testMultipleMarkdownInsideHTML() {
let html = MarkdownParser().html(from: """
<div>

![](image1.jpg)
![](image2.jpg)

</div>
""")

XCTAssertEqual(html, "<div><img src=\"image1.jpg\"/><img src=\"image2.jpg\"/></div>")
}

func testHTMLWithDoubleNewline() {
let src = """
<div>

<h1>Heading</h1>

</div>
"""
let html = MarkdownParser().html(from: src)

XCTAssertEqual(html, src)
}

func testUnclosedHTMLWithDoubleNewline() {
let src = """
<div>
*foo*

*bar*
"""
let html = MarkdownParser().html(from: src)

XCTAssertEqual(html, "<div>\n*foo*<p><em>bar</em></p>")
}

func testParagraphInsideHTML() {
let html = MarkdownParser().html(from: """
<div>

*Emphasized* text.

</div>
""")
XCTAssertEqual(html, "<div><p><em>Emphasized</em> text.</p></div>")
}

func testModifiersAppliedToMarkdownInsideHTML() {
var parser = MarkdownParser()
parser.addModifier(Modifier(target: .headings, closure: { input in return input.html+"<hr>"}))
let html = parser.html(from: """
<div>

# Heading

</div>
""")
XCTAssertEqual(html, "<div><h1>Heading</h1><hr></div>")
}
}

extension HTMLTests {
Expand All @@ -127,7 +251,17 @@ extension HTMLTests {
("testInlineSelfClosingHTMLElement", testInlineSelfClosingHTMLElement),
("testTopLevelHTMLLineBreak", testTopLevelHTMLLineBreak),
("testHTMLComment", testHTMLComment),
("testHTMLEntities", testHTMLEntities)
("testHTMLEntities", testHTMLEntities),
("testMarkdownHeadingInsideHTML", testMarkdownHeadingInsideHTML),
("testMarkdownImageInsideHTML", testMarkdownImageInsideHTML),
("testMarkdownListInsideHTML", testMarkdownListInsideHTML),
("testMarkdownBeforeHTML", testMarkdownBeforeHTML),
("testMarkdownAfterHTML", testMarkdownAfterHTML),
("testMultipleMarkdownInsideHTML", testMultipleMarkdownInsideHTML),
("testHTMLWithDoubleNewline", testHTMLWithDoubleNewline),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
("testHTMLWithDoubleNewline", testHTMLWithDoubleNewline),
("testHTMLWithDoubleNewline", testHTMLWithDoubleNewline),
("testUnclosedHTMLWithDoubleNewline", testUnclosedHTMLWithDoubleNewline),

Add extra text case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't seem to have included the test. I'll add it

("testUnclosedHTMLWithDoubleNewline", testUnclosedHTMLWithDoubleNewline),
("testParagraphInsideHTML", testParagraphInsideHTML),
("testModifiersAppliedToMarkdownInsideHTML", testModifiersAppliedToMarkdownInsideHTML),
]
}
}