MM-11175 Add logic to server to understand markdown images with dimensions (#9159)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d81a61398d
Коммит
ecfba2c2e9
@@ -295,6 +295,14 @@ func TestImageProxy(t *testing.T) {
|
|||||||
assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message)
|
assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(post).Message)
|
||||||
post.Message = ""
|
post.Message = ""
|
||||||
assert.Equal(t, "", th.App.PostWithProxyRemovedFromImageURLs(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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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] != '(' {
|
if position >= len(p.raw) || p.raw[position] != '(' {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -273,6 +273,23 @@ func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int) (destin
|
|||||||
}
|
}
|
||||||
position = end
|
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]) {
|
if position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
|
||||||
titleStart := nextNonWhitespace(p.raw, position)
|
titleStart := nextNonWhitespace(p.raw, position)
|
||||||
if titleStart >= len(p.raw) {
|
if titleStart >= len(p.raw) {
|
||||||
@@ -281,11 +298,13 @@ func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int) (destin
|
|||||||
return destination, Range{titleStart, titleStart}, titleStart + 1, true
|
return destination, Range{titleStart, titleStart}, titleStart + 1, true
|
||||||
}
|
}
|
||||||
|
|
||||||
title, end, ok = parseLinkTitle(p.raw, titleStart)
|
if p.raw[titleStart] == '"' || p.raw[titleStart] == '\'' || p.raw[titleStart] == '(' {
|
||||||
if !ok {
|
title, end, ok = parseLinkTitle(p.raw, titleStart)
|
||||||
return
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
position = end
|
||||||
}
|
}
|
||||||
position = end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closingPosition := nextNonWhitespace(p.raw, position)
|
closingPosition := nextNonWhitespace(p.raw, position)
|
||||||
@@ -317,9 +336,11 @@ func (p *inlineParser) lookForLinkOrImage() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isImage := d.Type == imageOpeningDelimiter
|
||||||
|
|
||||||
var inline Inline
|
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)
|
destinationMarkdownPosition := relativeToAbsolutePosition(p.ranges, destination.Position)
|
||||||
linkOrImage := InlineLinkOrImage{
|
linkOrImage := InlineLinkOrImage{
|
||||||
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
|
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
|
||||||
|
|||||||
@@ -128,3 +128,57 @@ func parseLinkLabel(markdown string, position int) (raw Range, next int, ok bool
|
|||||||
|
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|||||||
223
utils/markdown/links_test.go
Обычный файл
223
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: `<p><a href="https://example.com">link</a></p>`,
|
||||||
|
},
|
||||||
|
"image link": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with title": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with bracketed title": {
|
||||||
|
Markdown: `)`,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with width": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with width and title": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with width and bracketed title": {
|
||||||
|
Markdown: `)`,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with height": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with height and title": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with height and bracketed title": {
|
||||||
|
Markdown: `)`,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with dimensions": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with dimensions and title": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||||
|
},
|
||||||
|
"image link with dimensions and bracketed title": {
|
||||||
|
Markdown: `)`,
|
||||||
|
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: ``,
|
||||||
|
ExpectedHTML: `<p><img src="" alt="image" /></p>`,
|
||||||
|
},
|
||||||
|
"no image link with dimensions": {
|
||||||
|
Markdown: ``,
|
||||||
|
ExpectedHTML: `<p><img src="=500x400" alt="image" /></p>`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.ExpectedHTML, RenderHTML(tc.Markdown))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,8 +32,16 @@ func isWhitespaceByte(c byte) bool {
|
|||||||
return isWhitespace(rune(c))
|
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 {
|
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 {
|
func isHexByte(c byte) bool {
|
||||||
@@ -41,7 +49,7 @@ func isHexByte(c byte) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isAlphanumeric(c rune) 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 {
|
func isAlphanumericByte(c byte) bool {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user