summaryrefslogtreecommitdiffstats
path: root/utils
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2018-08-01 11:43:58 -0400
committerGitHub <noreply@github.com>2018-08-01 11:43:58 -0400
commitecfba2c2e9e46389e2012e9adf1ab993aaa7ea5e (patch)
tree9caa617309e989ba4aafae9dbfaebabd99a4e537 /utils
parentd81a61398d01d839e70e2345da787e7ef89c0832 (diff)
downloadchat-ecfba2c2e9e46389e2012e9adf1ab993aaa7ea5e.tar.gz
chat-ecfba2c2e9e46389e2012e9adf1ab993aaa7ea5e.tar.bz2
chat-ecfba2c2e9e46389e2012e9adf1ab993aaa7ea5e.zip
MM-11175 Add logic to server to understand markdown images with dimensions (#9159)
Diffstat (limited to 'utils')
-rw-r--r--utils/markdown/inlines.go33
-rw-r--r--utils/markdown/links.go54
-rw-r--r--utils/markdown/links_test.go223
-rw-r--r--utils/markdown/markdown.go12
4 files changed, 314 insertions, 8 deletions
diff --git a/utils/markdown/inlines.go b/utils/markdown/inlines.go
index e6943a57d..453f4bbe5 100644
--- a/utils/markdown/inlines.go
+++ b/utils/markdown/inlines.go
@@ -254,7 +254,7 @@ func (p *inlineParser) parseLinkOrImageDelimiter() {
}
}
-func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int) (destination, title Range, end int, ok bool) {
+func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int, isImage bool) (destination, title Range, end int, ok bool) {
if position >= len(p.raw) || p.raw[position] != '(' {
return
}
@@ -273,6 +273,23 @@ func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int) (destin
}
position = end
+ if isImage && position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
+ dimensionsStart := nextNonWhitespace(p.raw, position)
+ if dimensionsStart >= len(p.raw) {
+ return
+ }
+
+ if p.raw[dimensionsStart] == '=' {
+ // Read optional image dimensions even if we don't use them
+ _, end, ok = parseImageDimensions(p.raw, dimensionsStart)
+ if !ok {
+ return
+ }
+
+ position = end
+ }
+ }
+
if position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
titleStart := nextNonWhitespace(p.raw, position)
if titleStart >= len(p.raw) {
@@ -281,11 +298,13 @@ func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int) (destin
return destination, Range{titleStart, titleStart}, titleStart + 1, true
}
- title, end, ok = parseLinkTitle(p.raw, titleStart)
- if !ok {
- return
+ if p.raw[titleStart] == '"' || p.raw[titleStart] == '\'' || p.raw[titleStart] == '(' {
+ title, end, ok = parseLinkTitle(p.raw, titleStart)
+ if !ok {
+ return
+ }
+ position = end
}
- position = end
}
closingPosition := nextNonWhitespace(p.raw, position)
@@ -317,9 +336,11 @@ func (p *inlineParser) lookForLinkOrImage() {
break
}
+ isImage := d.Type == imageOpeningDelimiter
+
var inline Inline
- if destination, title, next, ok := p.peekAtInlineLinkDestinationAndTitle(p.position + 1); ok {
+ if destination, title, next, ok := p.peekAtInlineLinkDestinationAndTitle(p.position+1, isImage); ok {
destinationMarkdownPosition := relativeToAbsolutePosition(p.ranges, destination.Position)
linkOrImage := InlineLinkOrImage{
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
diff --git a/utils/markdown/links.go b/utils/markdown/links.go
index 419797cb9..9f3128c4f 100644
--- a/utils/markdown/links.go
+++ b/utils/markdown/links.go
@@ -128,3 +128,57 @@ func parseLinkLabel(markdown string, position int) (raw Range, next int, ok bool
return
}
+
+// As a non-standard feature, we allow image links to specify dimensions of the image by adding "=WIDTHxHEIGHT"
+// after the image destination but before the image title like ![alt](http://example.com/image.png =100x200 "title").
+// Both width and height are optional, but at least one of them must be specified.
+func parseImageDimensions(markdown string, position int) (raw Range, next int, ok bool) {
+ if position >= len(markdown) {
+ return
+ }
+
+ originalPosition := position
+
+ // Read =
+ position += 1
+ if position >= len(markdown) {
+ return
+ }
+
+ // Read width
+ hasWidth := false
+ for isNumericByte(markdown[position]) {
+ hasWidth = true
+ position += 1
+ }
+
+ // Look for early end of dimensions
+ if isWhitespaceByte(markdown[position]) || markdown[position] == ')' {
+ return Range{originalPosition, position - 1}, position, true
+ }
+
+ // Read the x
+ if markdown[position] != 'x' && markdown[position] != 'X' {
+ return
+ }
+ position += 1
+
+ // Read height
+ hasHeight := false
+ for isNumericByte(markdown[position]) {
+ hasHeight = true
+ position += 1
+ }
+
+ // Make sure the there's no trailing characters
+ if !isWhitespaceByte(markdown[position]) && markdown[position] != ')' {
+ return
+ }
+
+ if !hasWidth && !hasHeight {
+ // At least one of width or height is required
+ return
+ }
+
+ return Range{originalPosition, position - 1}, position, true
+}
diff --git a/utils/markdown/links_test.go b/utils/markdown/links_test.go
new file mode 100644
index 000000000..15012e26b
--- /dev/null
+++ b/utils/markdown/links_test.go
@@ -0,0 +1,223 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package markdown
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseImageDimensions(t *testing.T) {
+ for name, tc := range map[string]struct {
+ Input string
+ Position int
+ ExpectedRange Range
+ ExpectedNext int
+ ExpectedOk bool
+ }{
+ "no dimensions, no title": {
+ Input: `![alt](https://example.com)`,
+ Position: 26,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ "no dimensions, title": {
+ Input: `![alt](https://example.com "title")`,
+ Position: 27,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ "only width, no title": {
+ Input: `![alt](https://example.com =100)`,
+ Position: 27,
+ ExpectedRange: Range{27, 30},
+ ExpectedNext: 31,
+ ExpectedOk: true,
+ },
+ "only width, title": {
+ Input: `![alt](https://example.com =100 "title")`,
+ Position: 27,
+ ExpectedRange: Range{27, 30},
+ ExpectedNext: 31,
+ ExpectedOk: true,
+ },
+ "only height, no title": {
+ Input: `![alt](https://example.com =x100)`,
+ Position: 27,
+ ExpectedRange: Range{27, 31},
+ ExpectedNext: 32,
+ ExpectedOk: true,
+ },
+ "only height, title": {
+ Input: `![alt](https://example.com =x100 "title")`,
+ Position: 27,
+ ExpectedRange: Range{27, 31},
+ ExpectedNext: 32,
+ ExpectedOk: true,
+ },
+ "dimensions, no title": {
+ Input: `![alt](https://example.com =100x200)`,
+ Position: 27,
+ ExpectedRange: Range{27, 34},
+ ExpectedNext: 35,
+ ExpectedOk: true,
+ },
+ "dimensions, title": {
+ Input: `![alt](https://example.com =100x200 "title")`,
+ Position: 27,
+ ExpectedRange: Range{27, 34},
+ ExpectedNext: 35,
+ ExpectedOk: true,
+ },
+ "no dimensions, no title, trailing whitespace": {
+ Input: `![alt](https://example.com )`,
+ Position: 27,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ "only width, no title, trailing whitespace": {
+ Input: `![alt](https://example.com =100 )`,
+ Position: 28,
+ ExpectedRange: Range{28, 31},
+ ExpectedNext: 32,
+ ExpectedOk: true,
+ },
+ "only height, no title, trailing whitespace": {
+ Input: `![alt](https://example.com =x100 )`,
+ Position: 29,
+ ExpectedRange: Range{29, 33},
+ ExpectedNext: 34,
+ ExpectedOk: true,
+ },
+ "dimensions, no title, trailing whitespace": {
+ Input: `![alt](https://example.com =100x200 )`,
+ Position: 30,
+ ExpectedRange: Range{30, 37},
+ ExpectedNext: 38,
+ ExpectedOk: true,
+ },
+ "no width or height": {
+ Input: `![alt](https://example.com =x)`,
+ Position: 27,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ "garbage 1": {
+ Input: `![alt](https://example.com =aaa)`,
+ Position: 27,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ "garbage 2": {
+ Input: `![alt](https://example.com ====)`,
+ Position: 27,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ "garbage 3": {
+ Input: `![alt](https://example.com =100xx200)`,
+ Position: 27,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ "garbage 4": {
+ Input: `![alt](https://example.com =100x200x300x400)`,
+ Position: 27,
+ ExpectedRange: Range{0, 0},
+ ExpectedNext: 0,
+ ExpectedOk: false,
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ raw, next, ok := parseImageDimensions(tc.Input, tc.Position)
+ assert.Equal(t, tc.ExpectedOk, ok)
+ assert.Equal(t, tc.ExpectedNext, next)
+ assert.Equal(t, tc.ExpectedRange, raw)
+ })
+ }
+}
+
+func TestImageLinksWithDimensions(t *testing.T) {
+ for name, tc := range map[string]struct {
+ Markdown string
+ ExpectedHTML string
+ }{
+ "regular link": {
+ Markdown: `[link](https://example.com)`,
+ ExpectedHTML: `<p><a href="https://example.com">link</a></p>`,
+ },
+ "image link": {
+ Markdown: `![image](https://example.com/image.png)`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
+ },
+ "image link with title": {
+ Markdown: `![image](https://example.com/image.png "title")`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "image link with bracketed title": {
+ Markdown: `![image](https://example.com/image.png (title))`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "image link with width": {
+ Markdown: `![image](https://example.com/image.png =500)`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
+ },
+ "image link with width and title": {
+ Markdown: `![image](https://example.com/image.png =500 "title")`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "image link with width and bracketed title": {
+ Markdown: `![image](https://example.com/image.png =500 (title))`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "image link with height": {
+ Markdown: `![image](https://example.com/image.png =x500)`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
+ },
+ "image link with height and title": {
+ Markdown: `![image](https://example.com/image.png =x500 "title")`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "image link with height and bracketed title": {
+ Markdown: `![image](https://example.com/image.png =x500 (title))`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "image link with dimensions": {
+ Markdown: `![image](https://example.com/image.png =500x400)`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
+ },
+ "image link with dimensions and title": {
+ Markdown: `![image](https://example.com/image.png =500x400 "title")`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "image link with dimensions and bracketed title": {
+ Markdown: `![image](https://example.com/image.png =500x400 (title))`,
+ ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
+ },
+ "no image link 1": {
+ Markdown: `![image]()`,
+ ExpectedHTML: `<p><img src="" alt="image" /></p>`,
+ },
+ "no image link 2": {
+ Markdown: `![image]( )`,
+ ExpectedHTML: `<p><img src="" alt="image" /></p>`,
+ },
+ "no image link with dimensions": {
+ Markdown: `![image]( =500x400)`,
+ ExpectedHTML: `<p><img src="=500x400" alt="image" /></p>`,
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ assert.Equal(t, tc.ExpectedHTML, RenderHTML(tc.Markdown))
+ })
+ }
+}
diff --git a/utils/markdown/markdown.go b/utils/markdown/markdown.go
index e0788d906..57b10f3fb 100644
--- a/utils/markdown/markdown.go
+++ b/utils/markdown/markdown.go
@@ -32,8 +32,16 @@ func isWhitespaceByte(c byte) bool {
return isWhitespace(rune(c))
}
+func isNumeric(c rune) bool {
+ return c >= '0' && c <= '9'
+}
+
+func isNumericByte(c byte) bool {
+ return isNumeric(rune(c))
+}
+
func isHex(c rune) bool {
- return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
+ return isNumeric(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
func isHexByte(c byte) bool {
@@ -41,7 +49,7 @@ func isHexByte(c byte) bool {
}
func isAlphanumeric(c rune) bool {
- return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+ return isNumeric(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
func isAlphanumericByte(c byte) bool {