// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. // This package implements a parser for the subset of the CommonMark spec necessary for us to do // server-side processing. It is not a full implementation and lacks many features. But it is // complete enough to efficiently and accurately allow us to do what we need to like rewrite image // URLs for proxying. package markdown import ( "strings" ) func isEscapable(c rune) bool { return c > ' ' && (c < '0' || (c > '9' && (c < 'A' || (c > 'Z' && (c < 'a' || (c > 'z' && c <= '~')))))) } func isEscapableByte(c byte) bool { return isEscapable(rune(c)) } func isWhitespace(c rune) bool { switch c { case ' ', '\t', '\n', '\u000b', '\u000c', '\r': return true default: return false } } 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 isNumeric(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') } func isHexByte(c byte) bool { return isHex(rune(c)) } func isAlphanumeric(c rune) bool { return isNumeric(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } func isAlphanumericByte(c byte) bool { return isAlphanumeric(rune(c)) } func nextNonWhitespace(markdown string, position int) int { for offset, c := range []byte(markdown[position:]) { if !isWhitespaceByte(c) { return position + offset } } return len(markdown) } func nextLine(markdown string, position int) (linePosition int, skippedNonWhitespace bool) { for i := position; i < len(markdown); i++ { c := markdown[i] if c == '\r' { if i+1 < len(markdown) && markdown[i+1] == '\n' { return i + 2, skippedNonWhitespace } return i + 1, skippedNonWhitespace } else if c == '\n' { return i + 1, skippedNonWhitespace } else if !isWhitespaceByte(c) { skippedNonWhitespace = true } } return len(markdown), skippedNonWhitespace } func countIndentation(markdown string, r Range) (spaces, bytes int) { for i := r.Position; i < r.End; i++ { if markdown[i] == ' ' { spaces++ bytes++ } else if markdown[i] == '\t' { spaces += 4 bytes++ } else { break } } return } func trimLeftSpace(markdown string, r Range) Range { s := markdown[r.Position:r.End] trimmed := strings.TrimLeftFunc(s, isWhitespace) return Range{r.Position, r.End - (len(s) - len(trimmed))} } func trimRightSpace(markdown string, r Range) Range { s := markdown[r.Position:r.End] trimmed := strings.TrimRightFunc(s, isWhitespace) return Range{r.Position, r.End - (len(s) - len(trimmed))} } func relativeToAbsolutePosition(ranges []Range, position int) int { rem := position for _, r := range ranges { l := r.End - r.Position if rem < l { return r.Position + rem } rem -= l } if len(ranges) == 0 { return 0 } return ranges[len(ranges)-1].End } func trimBytesFromRanges(ranges []Range, bytes int) (result []Range) { rem := bytes for _, r := range ranges { if rem == 0 { result = append(result, r) continue } l := r.End - r.Position if rem < l { result = append(result, Range{r.Position + rem, r.End}) rem = 0 continue } rem -= l } return } func Parse(markdown string) (*Document, []*ReferenceDefinition) { lines := ParseLines(markdown) return ParseBlocks(markdown, lines) }