Update serverside markdown parser to respect unicode and capitalized links (#9235)
Этот коммит содержится в:
коммит произвёл
Jesús Espino
родитель
935f0c5ff9
Коммит
99cf15b56e
@@ -16,27 +16,27 @@ var (
|
|||||||
DefaultUrlSchemes = []string{"http", "https", "ftp", "mailto", "tel"}
|
DefaultUrlSchemes = []string{"http", "https", "ftp", "mailto", "tel"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Given a string with a w at the given position, tries to parse and return a link starting with "www."
|
// Given a string with a w at the given position, tries to parse and return a range containing a www link.
|
||||||
// if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to
|
// if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to
|
||||||
// www_match from the reference code.
|
// www_match from the reference code.
|
||||||
func parseWWWAutolink(data string, position int) string {
|
func parseWWWAutolink(data string, position int) (Range, bool) {
|
||||||
// Check that this isn't part of another word
|
// Check that this isn't part of another word
|
||||||
if position > 1 {
|
if position > 1 {
|
||||||
prevChar := data[position-1]
|
prevChar := data[position-1]
|
||||||
|
|
||||||
if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) {
|
if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that this starts with www
|
// Check that this starts with www
|
||||||
if len(data)-position < 4 || !regexp.MustCompile(`^www\d{0,3}\.`).MatchString(data[position:]) {
|
if len(data)-position < 4 || !regexp.MustCompile(`^www\d{0,3}\.`).MatchString(data[position:]) {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
end := checkDomain(data[position:], false)
|
end := checkDomain(data[position:], false)
|
||||||
if end == 0 {
|
if end == 0 {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
end += position
|
end += position
|
||||||
@@ -47,12 +47,12 @@ func parseWWWAutolink(data string, position int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trim trailing punctuation
|
// Trim trailing punctuation
|
||||||
link := trimTrailingCharactersFromLink(data[position:end])
|
end = trimTrailingCharactersFromLink(data, position, end)
|
||||||
if link == "" {
|
if position == end {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return link
|
return Range{position, end}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAllowedBeforeWWWLink(c byte) bool {
|
func isAllowedBeforeWWWLink(c byte) bool {
|
||||||
@@ -64,13 +64,13 @@ func isAllowedBeforeWWWLink(c byte) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a string with a : at the given position, tried to parse and return a link starting with a URL scheme
|
// Given a string with a : at the given position, tried to parse and return a range containing a URL scheme
|
||||||
// if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to
|
// if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to
|
||||||
// url_match from the reference code.
|
// url_match from the reference code.
|
||||||
func parseURLAutolink(data string, position int) string {
|
func parseURLAutolink(data string, position int) (Range, bool) {
|
||||||
// Check that a :// exists. This doesn't match the clients that treat the slashes as optional.
|
// Check that a :// exists. This doesn't match the clients that treat the slashes as optional.
|
||||||
if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' {
|
if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
start := position - 1
|
start := position - 1
|
||||||
@@ -81,12 +81,12 @@ func parseURLAutolink(data string, position int) string {
|
|||||||
// Ensure that the URL scheme is allowed and that at least one character after the scheme is valid.
|
// Ensure that the URL scheme is allowed and that at least one character after the scheme is valid.
|
||||||
scheme := data[start:position]
|
scheme := data[start:position]
|
||||||
if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) {
|
if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
end := checkDomain(data[position+3:], true)
|
end := checkDomain(data[position+3:], true)
|
||||||
if end == 0 {
|
if end == 0 {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
end += position
|
end += position
|
||||||
@@ -97,12 +97,12 @@ func parseURLAutolink(data string, position int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trim trailing punctuation
|
// Trim trailing punctuation
|
||||||
link := trimTrailingCharactersFromLink(data[start:end])
|
end = trimTrailingCharactersFromLink(data, start, end)
|
||||||
if link == "" {
|
if start == end {
|
||||||
return ""
|
return Range{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return link
|
return Range{start, end}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSchemeAllowed(scheme string) bool {
|
func isSchemeAllowed(scheme string) bool {
|
||||||
@@ -166,9 +166,9 @@ func isValidHostCharacter(link string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link.
|
// Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link.
|
||||||
// Equivalent to autolink_delim from the reference code.
|
// Returns a new end position for the link. Equivalent to autolink_delim from the reference code.
|
||||||
func trimTrailingCharactersFromLink(link string) string {
|
func trimTrailingCharactersFromLink(markdown string, start int, end int) int {
|
||||||
runes := []rune(link)
|
runes := []rune(markdown[start:end])
|
||||||
linkEnd := len(runes)
|
linkEnd := len(runes)
|
||||||
|
|
||||||
// Cut off the link before an open angle bracket if it contains one
|
// Cut off the link before an open angle bracket if it contains one
|
||||||
@@ -240,7 +240,7 @@ func trimTrailingCharactersFromLink(link string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(runes[:linkEnd])
|
return start + len(string(runes[:linkEnd]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func canEndAutolink(c rune) bool {
|
func canEndAutolink(c rune) bool {
|
||||||
|
|||||||
@@ -134,7 +134,15 @@ func TestParseURLAutolink(t *testing.T) {
|
|||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.Description, func(t *testing.T) {
|
t.Run(testCase.Description, func(t *testing.T) {
|
||||||
assert.Equal(t, testCase.Expected, parseURLAutolink(testCase.Input, testCase.Position))
|
rawRange, ok := parseURLAutolink(testCase.Input, testCase.Position)
|
||||||
|
|
||||||
|
if testCase.Expected == "" {
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Equal(t, Range{0, 0}, rawRange)
|
||||||
|
} else {
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, testCase.Expected, testCase.Input[rawRange.Position:rawRange.End])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,89 +272,153 @@ func TestParseWWWAutolink(t *testing.T) {
|
|||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.Description, func(t *testing.T) {
|
t.Run(testCase.Description, func(t *testing.T) {
|
||||||
assert.Equal(t, testCase.Expected, parseWWWAutolink(testCase.Input, testCase.Position))
|
rawRange, ok := parseWWWAutolink(testCase.Input, testCase.Position)
|
||||||
|
|
||||||
|
if testCase.Expected == "" {
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Equal(t, Range{0, 0}, rawRange)
|
||||||
|
} else {
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, testCase.Expected, testCase.Input[rawRange.Position:rawRange.End])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrimTrailingCharactersFromLink(t *testing.T) {
|
func TestTrimTrailingCharactersFromLink(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Expected string
|
Start int
|
||||||
|
End int
|
||||||
|
ExpectedEnd int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com",
|
Input: "http://www.example.com",
|
||||||
Expected: "http://www.example.com",
|
ExpectedEnd: 22,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com/abcd",
|
Input: "http://www.example.com/abcd",
|
||||||
Expected: "http://www.example.com/abcd",
|
ExpectedEnd: 27,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com/abcd/",
|
Input: "http://www.example.com/abcd/",
|
||||||
Expected: "http://www.example.com/abcd/",
|
ExpectedEnd: 28,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com/1234",
|
Input: "http://www.example.com/1234",
|
||||||
Expected: "http://www.example.com/1234",
|
ExpectedEnd: 27,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com/abcd?foo=bar",
|
Input: "http://www.example.com/abcd?foo=bar",
|
||||||
Expected: "http://www.example.com/abcd?foo=bar",
|
ExpectedEnd: 35,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com/abcd#heading",
|
Input: "http://www.example.com/abcd#heading",
|
||||||
Expected: "http://www.example.com/abcd#heading",
|
ExpectedEnd: 35,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com.",
|
Input: "http://www.example.com.",
|
||||||
Expected: "http://www.example.com",
|
ExpectedEnd: 22,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com,",
|
Input: "http://www.example.com,",
|
||||||
Expected: "http://www.example.com",
|
ExpectedEnd: 22,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com?",
|
Input: "http://www.example.com?",
|
||||||
Expected: "http://www.example.com",
|
ExpectedEnd: 22,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com)",
|
Input: "http://www.example.com)",
|
||||||
Expected: "http://www.example.com",
|
ExpectedEnd: 22,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com",
|
Input: "http://www.example.com",
|
||||||
Expected: "http://www.example.com",
|
ExpectedEnd: 22,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)",
|
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)",
|
||||||
Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)",
|
ExpectedEnd: 54,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation",
|
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation",
|
||||||
Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation",
|
ExpectedEnd: 53,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation))",
|
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation))",
|
||||||
Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)",
|
ExpectedEnd: 54,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)_(disambiguation)",
|
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)_(disambiguation)",
|
||||||
Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)_(disambiguation)",
|
ExpectedEnd: 71,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation_(disambiguation))",
|
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation_(disambiguation))",
|
||||||
Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation_(disambiguation))",
|
ExpectedEnd: 71,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: "http://www.example.com"",
|
Input: "http://www.example.com"",
|
||||||
Expected: "http://www.example.com",
|
ExpectedEnd: 22,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "this is a sentence containing http://www.example.com in it",
|
||||||
|
Start: 30,
|
||||||
|
End: 52,
|
||||||
|
ExpectedEnd: 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "this is a sentence containing http://www.example.com???",
|
||||||
|
Start: 30,
|
||||||
|
End: 55,
|
||||||
|
ExpectedEnd: 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "http://google.com/å",
|
||||||
|
ExpectedEnd: len("http://google.com/å"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "http://google.com/å...",
|
||||||
|
ExpectedEnd: len("http://google.com/å"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "This is http://google.com/å, a link, and http://google.com/å",
|
||||||
|
Start: 8,
|
||||||
|
End: len("This is http://google.com/å,"),
|
||||||
|
ExpectedEnd: len("This is http://google.com/å"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "This is http://google.com/å, a link, and http://google.com/å",
|
||||||
|
Start: 41,
|
||||||
|
End: len("This is http://google.com/å, a link, and http://google.com/å"),
|
||||||
|
ExpectedEnd: len("This is http://google.com/å, a link, and http://google.com/å"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "This is http://google.com/å, a link, and http://google.com/å.",
|
||||||
|
Start: 41,
|
||||||
|
End: len("This is http://google.com/å, a link, and http://google.com/å."),
|
||||||
|
ExpectedEnd: len("This is http://google.com/å, a link, and http://google.com/å"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "http://🍄.ga/ http://x🍄.ga/",
|
||||||
|
Start: 0,
|
||||||
|
End: len("http://🍄.ga/"),
|
||||||
|
ExpectedEnd: len("http://🍄.ga/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "http://🍄.ga/ http://x🍄.ga/",
|
||||||
|
Start: len("http://🍄.ga/ "),
|
||||||
|
End: len("http://🍄.ga/ http://x🍄.ga/"),
|
||||||
|
ExpectedEnd: len("http://🍄.ga/ http://x🍄.ga/"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.Input, func(t *testing.T) {
|
t.Run(testCase.Input, func(t *testing.T) {
|
||||||
assert.Equal(t, testCase.Expected, trimTrailingCharactersFromLink(testCase.Input))
|
if testCase.End == 0 {
|
||||||
|
testCase.End = len(testCase.Input) - testCase.Start
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.ExpectedEnd, trimTrailingCharactersFromLink(testCase.Input, testCase.Start, testCase.End))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1000,7 +1000,7 @@ func TestCommonMarkReferenceStrings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommonMarkRefernceAutolinks(t *testing.T) {
|
func TestCommonMarkReferenceAutolinks(t *testing.T) {
|
||||||
// These tests are adapted from the GitHub-flavoured CommonMark extension tests located at
|
// These tests are adapted from the GitHub-flavoured CommonMark extension tests located at
|
||||||
// https://github.com/github/cmark/blob/master/test/extensions.txt
|
// https://github.com/github/cmark/blob/master/test/extensions.txt
|
||||||
for name, tc := range map[string]struct {
|
for name, tc := range map[string]struct {
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ func RenderInlineHTML(inline Inline) (result string) {
|
|||||||
}
|
}
|
||||||
result += "</a>"
|
result += "</a>"
|
||||||
case *Autolink:
|
case *Autolink:
|
||||||
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Link)) + `">`
|
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `">`
|
||||||
for _, inline := range v.Children {
|
for _, inline := range v.Children {
|
||||||
result += RenderInlineHTML(inline)
|
result += RenderInlineHTML(inline)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,19 @@ type Autolink struct {
|
|||||||
|
|
||||||
Children []Inline
|
Children []Inline
|
||||||
|
|
||||||
Link string
|
RawDestination Range
|
||||||
|
|
||||||
|
markdown string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Autolink) Destination() string {
|
||||||
|
destination := Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
|
||||||
|
|
||||||
|
if strings.HasPrefix(destination, "www") {
|
||||||
|
destination = "http://" + destination
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
type delimiterType int
|
type delimiterType int
|
||||||
@@ -486,15 +498,18 @@ func (p *inlineParser) parseAutolink(c rune) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
link := ""
|
var link Range
|
||||||
text := ""
|
|
||||||
if c == ':' {
|
if c == ':' {
|
||||||
text = parseURLAutolink(p.raw, p.position)
|
var ok bool
|
||||||
link = text
|
link, ok = parseURLAutolink(p.raw, p.position)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Since the current position is at the colon, we have to rewind the parsing slightly so that
|
// Since the current position is at the colon, we have to rewind the parsing slightly so that
|
||||||
// we don't duplicate the URL scheme
|
// we don't duplicate the URL scheme
|
||||||
rewind := strings.Index(text, ":")
|
rewind := strings.Index(p.raw[link.Position:link.End], ":")
|
||||||
if rewind != -1 {
|
if rewind != -1 {
|
||||||
lastInline := p.inlines[len(p.inlines)-1]
|
lastInline := p.inlines[len(p.inlines)-1]
|
||||||
lastText, ok := lastInline.(*Text)
|
lastText, ok := lastInline.(*Text)
|
||||||
@@ -512,22 +527,30 @@ func (p *inlineParser) parseAutolink(c rune) bool {
|
|||||||
Range: Range{lastText.Range.Position, lastText.Range.End - rewind},
|
Range: Range{lastText.Range.Position, lastText.Range.End - rewind},
|
||||||
})
|
})
|
||||||
p.position -= rewind
|
p.position -= rewind
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if c == 'w' {
|
} else if c == 'w' || c == 'W' {
|
||||||
text = parseWWWAutolink(p.raw, p.position)
|
var ok bool
|
||||||
link = "http://" + text
|
link, ok = parseWWWAutolink(p.raw, p.position)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if text == "" {
|
linkMarkdownPosition := relativeToAbsolutePosition(p.ranges, link.Position)
|
||||||
return false
|
linkRange := Range{linkMarkdownPosition, linkMarkdownPosition + link.End - link.Position}
|
||||||
}
|
|
||||||
|
|
||||||
p.inlines = append(p.inlines, &Autolink{
|
p.inlines = append(p.inlines, &Autolink{
|
||||||
Link: link,
|
Children: []Inline{
|
||||||
Children: []Inline{&Text{Text: text}},
|
&Text{
|
||||||
|
Text: p.raw[link.Position:link.End],
|
||||||
|
Range: linkRange,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RawDestination: linkRange,
|
||||||
|
markdown: p.markdown,
|
||||||
})
|
})
|
||||||
p.position += len(text)
|
p.position += (link.End - link.Position)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user