diff --git a/app/post_test.go b/app/post_test.go index 186f960d5b..4821978338 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -295,6 +295,14 @@ func TestImageProxy(t *testing.T) { assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message) post.Message = "" assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message) + + if tc.ImageURL != "" { + post.Message = "" + assert.Equal(t, "", th.App.PostWithProxyAddedToImageURLs(post).Message) + assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message) + post.Message = "" + assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message) + } }) } } diff --git a/utils/markdown/inlines.go b/utils/markdown/inlines.go index e6943a57de..453f4bbe5d 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 419797cb90..9f3128c4fb 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 . +// 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 0000000000..15012e26bd --- /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: ``, + Position: 26, + ExpectedRange: Range{0, 0}, + ExpectedNext: 0, + ExpectedOk: false, + }, + "no dimensions, title": { + Input: ``, + Position: 27, + ExpectedRange: Range{0, 0}, + ExpectedNext: 0, + ExpectedOk: false, + }, + "only width, no title": { + Input: ``, + Position: 27, + ExpectedRange: Range{27, 30}, + ExpectedNext: 31, + ExpectedOk: true, + }, + "only width, title": { + Input: ``, + Position: 27, + ExpectedRange: Range{27, 30}, + ExpectedNext: 31, + ExpectedOk: true, + }, + "only height, no title": { + Input: ``, + Position: 27, + ExpectedRange: Range{27, 31}, + ExpectedNext: 32, + ExpectedOk: true, + }, + "only height, title": { + Input: ``, + Position: 27, + ExpectedRange: Range{27, 31}, + ExpectedNext: 32, + ExpectedOk: true, + }, + "dimensions, no title": { + Input: ``, + Position: 27, + ExpectedRange: Range{27, 34}, + ExpectedNext: 35, + ExpectedOk: true, + }, + "dimensions, title": { + Input: ``, + Position: 27, + ExpectedRange: Range{27, 34}, + ExpectedNext: 35, + ExpectedOk: true, + }, + "no dimensions, no title, trailing whitespace": { + Input: ``, + Position: 27, + ExpectedRange: Range{0, 0}, + ExpectedNext: 0, + ExpectedOk: false, + }, + "only width, no title, trailing whitespace": { + Input: ``, + Position: 28, + ExpectedRange: Range{28, 31}, + ExpectedNext: 32, + ExpectedOk: true, + }, + "only height, no title, trailing whitespace": { + Input: ``, + Position: 29, + ExpectedRange: Range{29, 33}, + ExpectedNext: 34, + ExpectedOk: true, + }, + "dimensions, no title, trailing whitespace": { + Input: ``, + Position: 30, + ExpectedRange: Range{30, 37}, + ExpectedNext: 38, + ExpectedOk: true, + }, + "no width or height": { + Input: ``, + Position: 27, + ExpectedRange: Range{0, 0}, + ExpectedNext: 0, + ExpectedOk: false, + }, + "garbage 1": { + Input: ``, + Position: 27, + ExpectedRange: Range{0, 0}, + ExpectedNext: 0, + ExpectedOk: false, + }, + "garbage 2": { + Input: ``, + Position: 27, + ExpectedRange: Range{0, 0}, + ExpectedNext: 0, + ExpectedOk: false, + }, + "garbage 3": { + Input: ``, + Position: 27, + ExpectedRange: Range{0, 0}, + ExpectedNext: 0, + ExpectedOk: false, + }, + "garbage 4": { + Input: ``, + 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: `
`, + }, + "image link": { + Markdown: ``, + ExpectedHTML: `










