2014-04-10 14:20:58 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2016-02-20 17:10:05 -05:00
package markdown
2014-04-10 14:20:58 -04:00
import (
"bytes"
"fmt"
2015-03-08 22:14:50 -06:00
"io"
2017-02-14 08:13:59 +07:00
"net/url"
2014-04-10 14:20:58 -04:00
"path"
"path/filepath"
"regexp"
"strings"
2015-11-20 05:37:51 -05:00
"github.com/Unknwon/com"
2016-02-20 17:10:05 -05:00
"github.com/microcosm-cc/bluemonday"
2014-10-04 17:15:22 -04:00
"github.com/russross/blackfriday"
2015-03-23 18:32:24 -04:00
"golang.org/x/net/html"
2014-10-04 17:15:22 -04:00
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
2014-04-10 14:20:58 -04:00
)
2016-11-25 09:58:05 +08:00
// Issue name styles
2016-04-22 17:28:08 -05:00
const (
2016-11-25 09:58:05 +08:00
IssueNameStyleNumeric = "numeric"
IssueNameStyleAlphanumeric = "alphanumeric"
2016-04-22 17:28:08 -05:00
)
2016-11-25 09:58:05 +08:00
// Sanitizer markdown sanitizer
2016-02-20 17:10:05 -05:00
var Sanitizer = bluemonday . UGCPolicy ( )
2016-01-31 15:38:20 -05:00
2016-02-20 17:10:05 -05:00
// BuildSanitizer initializes sanitizer with allowed attributes based on settings.
// This function should only be called once during entire application lifecycle.
func BuildSanitizer ( ) {
// Normal markdown-stuff
2017-02-14 08:13:59 +07:00
Sanitizer . AllowAttrs ( "class" ) . Matching ( regexp . MustCompile ( ` [\p { L}\p { N}\s\-_',:\[\]!\./\\\(\)&]* ` ) ) . OnElements ( "code" , "div" , "ul" , "ol" , "dl" )
2016-02-20 17:10:05 -05:00
// Checkboxes
Sanitizer . AllowAttrs ( "type" ) . Matching ( regexp . MustCompile ( ` ^checkbox$ ` ) ) . OnElements ( "input" )
Sanitizer . AllowAttrs ( "checked" , "disabled" ) . OnElements ( "input" )
2017-02-14 08:13:59 +07:00
Sanitizer . AllowNoAttrs ( ) . OnElements ( "label" )
2014-04-10 14:20:58 -04:00
2016-02-20 17:10:05 -05:00
// Custom URL-Schemes
Sanitizer . AllowURLSchemes ( setting . Markdown . CustomURLSchemes ... )
2014-04-10 14:20:58 -04:00
}
2016-02-20 17:10:05 -05:00
// IsMarkdownFile reports whether name looks like a Markdown file
// based on its extension.
2014-04-10 14:20:58 -04:00
func IsMarkdownFile ( name string ) bool {
2016-08-11 05:48:08 -07:00
extension := strings . ToLower ( filepath . Ext ( name ) )
2016-08-12 02:29:29 -07:00
for _ , ext := range setting . Markdown . FileExtensions {
2016-08-11 05:48:08 -07:00
if strings . ToLower ( ext ) == extension {
return true
}
2014-04-10 14:20:58 -04:00
}
return false
}
2016-02-20 17:10:05 -05:00
// IsReadmeFile reports whether name looks like a README file
2017-02-14 08:13:59 +07:00
// based on its name.
2014-04-10 14:20:58 -04:00
func IsReadmeFile ( name string ) bool {
name = strings . ToLower ( name )
if len ( name ) < 6 {
return false
2015-02-02 23:04:36 -05:00
} else if len ( name ) == 6 {
2016-02-20 17:10:05 -05:00
return name == "readme"
2014-04-10 14:20:58 -04:00
}
2016-02-20 17:10:05 -05:00
return name [ : 7 ] == "readme."
2014-04-10 14:20:58 -04:00
}
2016-01-09 10:59:04 +08:00
var (
2016-02-20 17:10:05 -05:00
// MentionPattern matches string that mentions someone, e.g. @Unknwon
2016-10-17 04:17:59 +02:00
MentionPattern = regexp . MustCompile ( ` (\s|^|\W)@[0-9a-zA-Z-_\.]+ ` )
2016-02-20 17:10:05 -05:00
2016-04-22 17:28:08 -05:00
// IssueNumericPattern matches string that references to a numeric issue, e.g. #1287
IssueNumericPattern = regexp . MustCompile ( ` ( |^|\()#[0-9]+\b ` )
// IssueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
IssueAlphanumericPattern = regexp . MustCompile ( ` ( |^|\()[A-Z] { 1,10}-[1-9][0-9]*\b ` )
2016-12-26 11:52:04 +01:00
// CrossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. gogits/gogs#12345
CrossReferenceIssueNumericPattern = regexp . MustCompile ( ` ( |^)[0-9a-zA-Z]+/[0-9a-zA-Z]+#[0-9]+\b ` )
2016-02-20 17:10:05 -05:00
// Sha1CurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
2017-02-14 08:13:59 +07:00
// FIXME: this pattern matches pure numbers as well, right now we do a hack to check in renderSha1CurrentPattern
2016-08-15 15:27:19 -07:00
// by converting string to a number.
2017-02-14 08:13:59 +07:00
Sha1CurrentPattern = regexp . MustCompile ( ` (?:^|\s|\()[0-9a-f] { 40}\b ` )
// ShortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
ShortLinkPattern = regexp . MustCompile ( ` (\[\[.*\]\]\w*) ` )
// AnySHA1Pattern allows to split url containing SHA into parts
AnySHA1Pattern = regexp . MustCompile ( ` http\S+//(\S+)/(\S+)/(\S+)/(\S+)/([0-9a-f] { 40})(?:/?([^#\s]+)?(?:#(\S+))?)? ` )
// IssueFullPattern allows to split issue (and pull) URLs into parts
IssueFullPattern = regexp . MustCompile ( ` (?:^|\s|\()http\S+//((?:[^\s/]+/)+)((?:\w { 1,10}-)?[1-9][0-9]*)([\?|#]\S+.(\S+)?)?\b ` )
validLinksPattern = regexp . MustCompile ( ` ^[a-z][\w-]+:// ` )
2016-01-09 10:59:04 +08:00
)
2017-02-14 08:13:59 +07:00
// isLink reports whether link fits valid format.
func isLink ( link [ ] byte ) bool {
return validLinksPattern . Match ( link )
}
2016-07-16 00:36:39 +08:00
// FindAllMentions matches mention patterns in given content
// and returns a list of found user names without @ prefix.
func FindAllMentions ( content string ) [ ] string {
mentions := MentionPattern . FindAllString ( content , - 1 )
for i := range mentions {
2016-10-17 04:17:59 +02:00
mentions [ i ] = mentions [ i ] [ strings . Index ( mentions [ i ] , "@" ) + 1 : ] // Strip @ character
2016-07-16 00:36:39 +08:00
}
return mentions
}
2016-02-20 17:10:05 -05:00
// Renderer is a extended version of underlying render object.
type Renderer struct {
2014-10-04 17:15:22 -04:00
blackfriday . Renderer
2017-02-14 08:13:59 +07:00
urlPrefix string
isWikiMarkdown bool
2014-04-10 14:20:58 -04:00
}
2016-02-20 17:10:05 -05:00
// Link defines how formal links should be processed to produce corresponding HTML elements.
func ( r * Renderer ) Link ( out * bytes . Buffer , link [ ] byte , title [ ] byte , content [ ] byte ) {
2014-04-10 14:20:58 -04:00
if len ( link ) > 0 && ! isLink ( link ) {
2016-02-20 17:10:05 -05:00
if link [ 0 ] != '#' {
2017-02-14 08:13:59 +07:00
mLink := URLJoin ( r . urlPrefix , string ( link ) )
if r . isWikiMarkdown {
mLink = URLJoin ( r . urlPrefix , "wiki" , string ( link ) )
}
link = [ ] byte ( mLink )
2016-01-09 10:59:04 +08:00
}
}
r . Renderer . Link ( out , link , title , content )
}
2017-02-14 08:13:59 +07:00
// List renders markdown bullet or digit lists to HTML
func ( r * Renderer ) List ( out * bytes . Buffer , text func ( ) bool , flags int ) {
marker := out . Len ( )
if out . Len ( ) > 0 {
out . WriteByte ( '\n' )
2016-01-09 10:59:04 +08:00
}
2017-02-14 08:13:59 +07:00
if flags & blackfriday . LIST_TYPE_DEFINITION != 0 {
out . WriteString ( "<dl>" )
} else if flags & blackfriday . LIST_TYPE_ORDERED != 0 {
out . WriteString ( "<ol class='ui list'>" )
} else {
out . WriteString ( "<ul class='ui list'>" )
}
if ! text ( ) {
out . Truncate ( marker )
return
}
if flags & blackfriday . LIST_TYPE_DEFINITION != 0 {
out . WriteString ( "</dl>\n" )
} else if flags & blackfriday . LIST_TYPE_ORDERED != 0 {
out . WriteString ( "</ol>\n" )
} else {
out . WriteString ( "</ul>\n" )
2014-04-10 14:20:58 -04:00
}
}
2016-02-20 17:10:05 -05:00
// ListItem defines how list items should be processed to produce corresponding HTML elements.
2016-11-25 09:58:05 +08:00
func ( r * Renderer ) ListItem ( out * bytes . Buffer , text [ ] byte , flags int ) {
2016-02-20 17:10:05 -05:00
// Detect procedures to draw checkboxes.
2017-02-14 08:13:59 +07:00
prefix := ""
if bytes . HasPrefix ( text , [ ] byte ( "<p>" ) ) {
prefix = "<p>"
}
2016-01-13 13:25:52 +01:00
switch {
2017-02-14 08:13:59 +07:00
case bytes . HasPrefix ( text , [ ] byte ( prefix + "[ ] " ) ) :
text = append ( [ ] byte ( ` <div class="ui fitted disabled checkbox"><input type="checkbox" disabled="disabled" /><label /></div> ` ) , text [ 3 + len ( prefix ) : ] ... )
case bytes . HasPrefix ( text , [ ] byte ( prefix + "[x] " ) ) :
text = append ( [ ] byte ( ` <div class="ui checked fitted disabled checkbox"><input type="checkbox" checked="" disabled="disabled" /><label /></div> ` ) , text [ 3 + len ( prefix ) : ] ... )
}
if prefix != "" {
text = bytes . Replace ( text , [ ] byte ( "</p>" ) , [ ] byte { } , 1 )
2016-01-13 13:25:52 +01:00
}
2016-11-25 09:58:05 +08:00
r . Renderer . ListItem ( out , text , flags )
2016-01-13 13:25:52 +01:00
}
2016-02-20 17:10:05 -05:00
// Note: this section is for purpose of increase performance and
// reduce memory allocation at runtime since they are constant literals.
2015-11-20 05:37:51 -05:00
var (
svgSuffix = [ ] byte ( ".svg" )
svgSuffixWithMark = [ ] byte ( ".svg?" )
)
2016-02-20 17:10:05 -05:00
// Image defines how images should be processed to produce corresponding HTML elements.
func ( r * Renderer ) Image ( out * bytes . Buffer , link [ ] byte , title [ ] byte , alt [ ] byte ) {
2017-02-14 08:13:59 +07:00
prefix := r . urlPrefix
if r . isWikiMarkdown {
prefix = URLJoin ( prefix , "wiki" , "src" )
}
prefix = strings . Replace ( prefix , "/src/" , "/raw/" , 1 )
2015-11-20 05:37:51 -05:00
if len ( link ) > 0 {
if isLink ( link ) {
// External link with .svg suffix usually means CI status.
2016-02-20 17:10:05 -05:00
// TODO: define a keyword to allow non-svg images render as external link.
2015-11-20 05:37:51 -05:00
if bytes . HasSuffix ( link , svgSuffix ) || bytes . Contains ( link , svgSuffixWithMark ) {
2016-01-09 10:59:04 +08:00
r . Renderer . Image ( out , link , title , alt )
2015-11-20 05:37:51 -05:00
return
}
} else {
if link [ 0 ] != '/' {
2017-02-14 08:13:59 +07:00
if ! strings . HasSuffix ( prefix , "/" ) {
prefix += "/"
}
2015-11-20 05:37:51 -05:00
}
2017-02-14 08:13:59 +07:00
link = [ ] byte ( url . QueryEscape ( prefix + string ( link ) ) )
2015-11-06 11:10:27 -05:00
}
2014-10-14 23:44:34 -04:00
}
2015-11-06 11:10:27 -05:00
out . WriteString ( ` <a href=" ` )
out . Write ( link )
out . WriteString ( ` "> ` )
2016-01-09 10:59:04 +08:00
r . Renderer . Image ( out , link , title , alt )
2015-11-06 11:10:27 -05:00
out . WriteString ( "</a>" )
2014-10-14 23:44:34 -04:00
}
2016-02-20 17:10:05 -05:00
// cutoutVerbosePrefix cutouts URL prefix including sub-path to
// return a clean unified string of request URL path.
2015-11-15 17:37:26 -05:00
func cutoutVerbosePrefix ( prefix string ) string {
2016-07-16 00:36:39 +08:00
if len ( prefix ) == 0 || prefix [ 0 ] != '/' {
return prefix
}
2015-11-15 17:37:26 -05:00
count := 0
for i := 0 ; i < len ( prefix ) ; i ++ {
if prefix [ i ] == '/' {
count ++
}
2016-11-27 18:14:25 +08:00
if count >= 3 + setting . AppSubURLDepth {
2015-11-15 17:37:26 -05:00
return prefix [ : i ]
}
2015-11-15 22:22:25 +01:00
}
2015-11-15 17:37:26 -05:00
return prefix
}
2017-02-14 08:13:59 +07:00
// URLJoin joins url components, like path.Join, but preserving contents
func URLJoin ( elem ... string ) string {
res := ""
last := len ( elem ) - 1
for i , item := range elem {
res += item
if ! strings . HasSuffix ( res , "/" ) && i != last {
res += "/"
}
}
return res
}
2016-02-20 17:10:05 -05:00
// RenderIssueIndexPattern renders issue indexes to corresponding links.
2015-12-04 21:30:33 -05:00
func RenderIssueIndexPattern ( rawBytes [ ] byte , urlPrefix string , metas map [ string ] string ) [ ] byte {
2015-11-15 17:37:26 -05:00
urlPrefix = cutoutVerbosePrefix ( urlPrefix )
2016-04-22 17:28:08 -05:00
pattern := IssueNumericPattern
2016-11-25 09:58:05 +08:00
if metas [ "style" ] == IssueNameStyleAlphanumeric {
2016-04-22 17:28:08 -05:00
pattern = IssueAlphanumericPattern
}
ms := pattern . FindAll ( rawBytes , - 1 )
2014-04-10 14:20:58 -04:00
for _ , m := range ms {
2016-06-29 10:07:39 -05:00
if m [ 0 ] == ' ' || m [ 0 ] == '(' {
2016-04-22 17:28:08 -05:00
m = m [ 1 : ] // ignore leading space or opening parentheses
2015-03-23 18:32:24 -04:00
}
2016-04-22 17:28:08 -05:00
var link string
2015-12-04 21:30:33 -05:00
if metas == nil {
2017-02-14 08:13:59 +07:00
link = fmt . Sprintf ( ` <a href="%s">%s</a> ` , URLJoin ( urlPrefix , "issues" , string ( m [ 1 : ] ) ) , m )
2015-12-04 21:30:33 -05:00
} else {
// Support for external issue tracker
2016-11-25 09:58:05 +08:00
if metas [ "style" ] == IssueNameStyleAlphanumeric {
2016-04-22 17:28:08 -05:00
metas [ "index" ] = string ( m )
} else {
metas [ "index" ] = string ( m [ 1 : ] )
}
link = fmt . Sprintf ( ` <a href="%s">%s</a> ` , com . Expand ( metas [ "format" ] , metas ) , m )
2015-12-04 21:30:33 -05:00
}
2016-04-22 17:28:08 -05:00
rawBytes = bytes . Replace ( rawBytes , m , [ ] byte ( link ) , 1 )
2015-12-04 21:30:33 -05:00
}
return rawBytes
}
2017-02-14 08:13:59 +07:00
// IsSameDomain checks if given url string has the same hostname as current Gitea instance
func IsSameDomain ( s string ) bool {
if uapp , err := url . Parse ( setting . AppURL ) ; err == nil {
if u , err := url . Parse ( s ) ; err == nil {
return u . Host == uapp . Host
}
return false
}
return false
}
// renderFullSha1Pattern renders SHA containing URLs
func renderFullSha1Pattern ( rawBytes [ ] byte , urlPrefix string ) [ ] byte {
ms := AnySHA1Pattern . FindAllSubmatch ( rawBytes , - 1 )
for _ , m := range ms {
all := m [ 0 ]
paths := string ( m [ 1 ] )
var path = "//" + paths
author := string ( m [ 2 ] )
repoName := string ( m [ 3 ] )
path = URLJoin ( path , author , repoName )
ltype := "src"
itemType := m [ 4 ]
if IsSameDomain ( paths ) {
ltype = string ( itemType )
} else if string ( itemType ) == "commit" {
ltype = "commit"
}
sha := m [ 5 ]
var subtree string
if len ( m ) > 6 && len ( m [ 6 ] ) > 0 {
subtree = string ( m [ 6 ] )
}
var line [ ] byte
if len ( m ) > 7 && len ( m [ 7 ] ) > 0 {
line = m [ 7 ]
}
urlSuffix := ""
text := base . ShortSha ( string ( sha ) )
if subtree != "" {
urlSuffix = "/" + subtree
text += urlSuffix
}
if line != nil {
value := string ( line )
urlSuffix += "#"
urlSuffix += value
text += " ("
text += value
text += ")"
}
rawBytes = bytes . Replace ( rawBytes , all , [ ] byte ( fmt . Sprintf (
` <a href="%s">%s</a> ` , URLJoin ( path , ltype , string ( sha ) ) + urlSuffix , text ) ) , - 1 )
}
return rawBytes
}
// renderFullIssuePattern renders issues-like URLs
func renderFullIssuePattern ( rawBytes [ ] byte , urlPrefix string ) [ ] byte {
ms := IssueFullPattern . FindAllSubmatch ( rawBytes , - 1 )
for _ , m := range ms {
all := m [ 0 ]
paths := bytes . Split ( m [ 1 ] , [ ] byte ( "/" ) )
paths = paths [ : len ( paths ) - 1 ]
if bytes . HasPrefix ( paths [ 0 ] , [ ] byte ( "gist." ) ) {
continue
}
var path string
if len ( paths ) > 3 {
// Internal one
path = URLJoin ( urlPrefix , "issues" )
} else {
path = "//" + string ( m [ 1 ] )
}
id := string ( m [ 2 ] )
path = URLJoin ( path , id )
var comment [ ] byte
if len ( m ) > 3 {
comment = m [ 3 ]
}
urlSuffix := ""
text := "#" + id
if comment != nil {
urlSuffix += string ( comment )
text += " <i class='comment icon'></i>"
}
rawBytes = bytes . Replace ( rawBytes , all , [ ] byte ( fmt . Sprintf (
` <a href="%s%s">%s</a> ` , path , urlSuffix , text ) ) , - 1 )
}
return rawBytes
}
func firstIndexOfByte ( sl [ ] byte , target byte ) int {
for i := 0 ; i < len ( sl ) ; i ++ {
if sl [ i ] == target {
return i
}
}
return - 1
}
func lastIndexOfByte ( sl [ ] byte , target byte ) int {
for i := len ( sl ) - 1 ; i >= 0 ; i -- {
if sl [ i ] == target {
return i
}
}
return - 1
}
// renderShortLinks processes [[syntax]]
func renderShortLinks ( rawBytes [ ] byte , urlPrefix string , noLink bool ) [ ] byte {
ms := ShortLinkPattern . FindAll ( rawBytes , - 1 )
for _ , m := range ms {
orig := bytes . TrimSpace ( m )
m = orig [ 2 : ]
tailPos := lastIndexOfByte ( m , ']' ) + 1
tail := [ ] byte { }
if tailPos < len ( m ) {
tail = m [ tailPos : ]
m = m [ : tailPos - 1 ]
}
m = m [ : len ( m ) - 2 ]
props := map [ string ] string { }
// MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
// It makes page handling terrible, but we prefer GitHub syntax
// And fall back to MediaWiki only when it is obvious from the look
// Of text and link contents
sl := bytes . Split ( m , [ ] byte ( "|" ) )
for _ , v := range sl {
switch bytes . Count ( v , [ ] byte ( "=" ) ) {
// Piped args without = sign, these are mandatory arguments
case 0 :
{
sv := string ( v )
if props [ "name" ] == "" {
if isLink ( v ) {
// If we clearly see it is a link, we save it so
// But first we need to ensure, that if both mandatory args provided
// look like links, we stick to GitHub syntax
if props [ "link" ] != "" {
props [ "name" ] = props [ "link" ]
}
props [ "link" ] = strings . TrimSpace ( sv )
} else {
props [ "name" ] = sv
}
} else {
props [ "link" ] = strings . TrimSpace ( sv )
}
}
// Piped args with = sign, these are optional arguments
case 1 :
{
sep := firstIndexOfByte ( v , '=' )
key , val := string ( v [ : sep ] ) , html . UnescapeString ( string ( v [ sep + 1 : ] ) )
lastCharIndex := len ( val ) - 1
if ( val [ 0 ] == '"' || val [ 0 ] == '\'' ) && ( val [ lastCharIndex ] == '"' || val [ lastCharIndex ] == '\'' ) {
val = val [ 1 : lastCharIndex ]
}
props [ key ] = val
}
}
}
var name string
var link string
if props [ "link" ] != "" {
link = props [ "link" ]
} else if props [ "name" ] != "" {
link = props [ "name" ]
}
if props [ "title" ] != "" {
name = props [ "title" ]
} else if props [ "name" ] != "" {
name = props [ "name" ]
} else {
name = link
}
name += string ( tail )
image := false
ext := filepath . Ext ( string ( link ) )
if ext != "" {
switch ext {
case ".jpg" , ".jpeg" , ".png" , ".tif" , ".tiff" , ".webp" , ".gif" , ".bmp" , ".ico" , ".svg" :
{
image = true
}
}
}
absoluteLink := isLink ( [ ] byte ( link ) )
if ! absoluteLink {
link = url . QueryEscape ( link )
}
if image {
if ! absoluteLink {
link = URLJoin ( urlPrefix , "wiki" , "raw" , link )
}
title := props [ "title" ]
if title == "" {
title = props [ "alt" ]
}
if title == "" {
title = path . Base ( string ( name ) )
}
alt := props [ "alt" ]
if alt == "" {
alt = name
}
if alt != "" {
alt = ` alt=" ` + alt + ` " `
}
name = fmt . Sprintf ( ` <img src="%s" %s title="%s" /> ` , link , alt , title )
} else if ! absoluteLink {
link = URLJoin ( urlPrefix , "wiki" , link )
}
if noLink {
rawBytes = bytes . Replace ( rawBytes , orig , [ ] byte ( name ) , - 1 )
} else {
rawBytes = bytes . Replace ( rawBytes , orig ,
[ ] byte ( fmt . Sprintf ( ` <a href="%s">%s</a> ` , link , name ) ) , - 1 )
}
}
return rawBytes
}
2016-12-26 11:52:04 +01:00
// RenderCrossReferenceIssueIndexPattern renders issue indexes from other repositories to corresponding links.
func RenderCrossReferenceIssueIndexPattern ( rawBytes [ ] byte , urlPrefix string , metas map [ string ] string ) [ ] byte {
ms := CrossReferenceIssueNumericPattern . FindAll ( rawBytes , - 1 )
for _ , m := range ms {
if m [ 0 ] == ' ' || m [ 0 ] == '(' {
m = m [ 1 : ] // ignore leading space or opening parentheses
}
repo := string ( bytes . Split ( m , [ ] byte ( "#" ) ) [ 0 ] )
issue := string ( bytes . Split ( m , [ ] byte ( "#" ) ) [ 1 ] )
2017-02-14 08:13:59 +07:00
link := fmt . Sprintf ( ` <a href="%s">%s</a> ` , URLJoin ( urlPrefix , repo , "issues" , issue ) , m )
2016-12-26 11:52:04 +01:00
rawBytes = bytes . Replace ( rawBytes , m , [ ] byte ( link ) , 1 )
}
return rawBytes
}
2017-02-14 08:13:59 +07:00
// renderSha1CurrentPattern renders SHA1 strings to corresponding links that assumes in the same repository.
func renderSha1CurrentPattern ( rawBytes [ ] byte , urlPrefix string ) [ ] byte {
ms := Sha1CurrentPattern . FindAllSubmatch ( rawBytes , - 1 )
for _ , m := range ms {
all := m [ 0 ]
if com . StrTo ( all ) . MustInt ( ) > 0 {
continue
2016-08-15 15:27:19 -07:00
}
2017-02-14 08:13:59 +07:00
rawBytes = bytes . Replace ( rawBytes , all , [ ] byte ( fmt . Sprintf (
` <a href="%s">%s</a> ` , URLJoin ( urlPrefix , "commit" , string ( all ) ) , base . ShortSha ( string ( all ) ) ) ) , - 1 )
}
return rawBytes
2016-02-20 17:10:05 -05:00
}
// RenderSpecialLink renders mentions, indexes and SHA1 strings to corresponding links.
2015-12-04 21:30:33 -05:00
func RenderSpecialLink ( rawBytes [ ] byte , urlPrefix string , metas map [ string ] string ) [ ] byte {
ms := MentionPattern . FindAll ( rawBytes , - 1 )
for _ , m := range ms {
2016-10-17 04:17:59 +02:00
m = m [ bytes . Index ( m , [ ] byte ( "@" ) ) : ]
2015-12-04 21:30:33 -05:00
rawBytes = bytes . Replace ( rawBytes , m ,
2017-02-14 08:13:59 +07:00
[ ] byte ( fmt . Sprintf ( ` <a href="%s">%s</a> ` , URLJoin ( setting . AppURL , string ( m [ 1 : ] ) ) , m ) ) , - 1 )
2015-12-04 21:30:33 -05:00
}
2017-02-14 08:13:59 +07:00
rawBytes = renderShortLinks ( rawBytes , urlPrefix , false )
2015-12-04 21:30:33 -05:00
rawBytes = RenderIssueIndexPattern ( rawBytes , urlPrefix , metas )
2016-12-26 11:52:04 +01:00
rawBytes = RenderCrossReferenceIssueIndexPattern ( rawBytes , urlPrefix , metas )
2017-02-14 08:13:59 +07:00
rawBytes = renderFullSha1Pattern ( rawBytes , urlPrefix )
rawBytes = renderSha1CurrentPattern ( rawBytes , urlPrefix )
rawBytes = renderFullIssuePattern ( rawBytes , urlPrefix )
2015-12-04 21:30:33 -05:00
return rawBytes
}
2016-02-20 17:10:05 -05:00
// RenderRaw renders Markdown to HTML without handling special links.
2017-02-14 08:13:59 +07:00
func RenderRaw ( body [ ] byte , urlPrefix string , wikiMarkdown bool ) [ ] byte {
2014-04-10 14:20:58 -04:00
htmlFlags := 0
2014-10-04 17:15:22 -04:00
htmlFlags |= blackfriday . HTML_SKIP_STYLE
htmlFlags |= blackfriday . HTML_OMIT_CONTENTS
2016-02-20 17:10:05 -05:00
renderer := & Renderer {
2017-02-14 08:13:59 +07:00
Renderer : blackfriday . HtmlRenderer ( htmlFlags , "" , "" ) ,
urlPrefix : urlPrefix ,
isWikiMarkdown : wikiMarkdown ,
2014-04-10 14:20:58 -04:00
}
// set up the parser
extensions := 0
2014-10-04 17:15:22 -04:00
extensions |= blackfriday . EXTENSION_NO_INTRA_EMPHASIS
extensions |= blackfriday . EXTENSION_TABLES
extensions |= blackfriday . EXTENSION_FENCED_CODE
extensions |= blackfriday . EXTENSION_STRIKETHROUGH
extensions |= blackfriday . EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
2015-09-01 08:32:02 -04:00
if setting . Markdown . EnableHardLineBreak {
extensions |= blackfriday . EXTENSION_HARD_LINE_BREAK
}
2014-10-04 17:15:22 -04:00
body = blackfriday . Markdown ( body , renderer , extensions )
2014-05-05 13:08:01 -04:00
return body
}
2015-11-20 01:52:11 -05:00
var (
leftAngleBracket = [ ] byte ( "</" )
rightAngleBracket = [ ] byte ( ">" )
)
2015-11-20 05:37:51 -05:00
var noEndTags = [ ] string { "img" , "input" , "br" , "hr" }
2016-02-20 17:10:05 -05:00
// PostProcess treats different types of HTML differently,
2015-03-23 18:32:24 -04:00
// and only renders special links for plain text blocks.
2016-11-25 09:58:05 +08:00
func PostProcess ( rawHTML [ ] byte , urlPrefix string , metas map [ string ] string ) [ ] byte {
2015-11-20 05:37:51 -05:00
startTags := make ( [ ] string , 0 , 5 )
2015-03-08 22:14:50 -06:00
var buf bytes . Buffer
2016-11-25 09:58:05 +08:00
tokenizer := html . NewTokenizer ( bytes . NewReader ( rawHTML ) )
2015-11-20 05:37:51 -05:00
OUTER_LOOP :
2015-03-08 22:14:50 -06:00
for html . ErrorToken != tokenizer . Next ( ) {
token := tokenizer . Token ( )
switch token . Type {
2015-03-23 18:32:24 -04:00
case html . TextToken :
2015-12-04 21:30:33 -05:00
buf . Write ( RenderSpecialLink ( [ ] byte ( token . String ( ) ) , urlPrefix , metas ) )
2015-03-23 18:32:24 -04:00
case html . StartTagToken :
buf . WriteString ( token . String ( ) )
tagName := token . Data
// If this is an excluded tag, we skip processing all output until a close tag is encountered.
if strings . EqualFold ( "a" , tagName ) || strings . EqualFold ( "code" , tagName ) || strings . EqualFold ( "pre" , tagName ) {
2015-11-21 21:06:11 -05:00
stackNum := 1
2015-03-23 18:32:24 -04:00
for html . ErrorToken != tokenizer . Next ( ) {
token = tokenizer . Token ( )
2015-11-20 05:37:51 -05:00
2015-03-23 18:32:24 -04:00
// Copy the token to the output verbatim
2017-02-14 08:13:59 +07:00
buf . Write ( renderShortLinks ( [ ] byte ( token . String ( ) ) , urlPrefix , true ) )
2015-11-21 21:06:11 -05:00
if token . Type == html . StartTagToken {
2017-02-14 08:13:59 +07:00
if ! com . IsSliceContainsStr ( noEndTags , token . Data ) {
stackNum ++
}
2015-11-21 21:06:11 -05:00
}
// If this is the close tag to the outer-most, we are done
2016-02-19 17:39:50 -05:00
if token . Type == html . EndTagToken {
2015-11-21 21:06:11 -05:00
stackNum --
2016-02-19 17:39:50 -05:00
if stackNum <= 0 && strings . EqualFold ( tagName , token . Data ) {
2015-11-21 21:06:11 -05:00
break
}
2015-03-08 22:14:50 -06:00
}
}
2015-11-20 05:37:51 -05:00
continue OUTER_LOOP
}
if ! com . IsSliceContainsStr ( noEndTags , token . Data ) {
startTags = append ( startTags , token . Data )
2015-03-23 18:32:24 -04:00
}
2015-03-08 22:14:50 -06:00
2015-11-20 01:52:11 -05:00
case html . EndTagToken :
2015-11-24 19:28:24 -05:00
if len ( startTags ) == 0 {
2015-11-24 19:29:35 -05:00
buf . WriteString ( token . String ( ) )
2015-11-24 19:28:24 -05:00
break
}
2015-11-20 01:52:11 -05:00
buf . Write ( leftAngleBracket )
2015-11-20 05:37:51 -05:00
buf . WriteString ( startTags [ len ( startTags ) - 1 ] )
2015-11-20 01:52:11 -05:00
buf . Write ( rightAngleBracket )
2015-11-20 05:37:51 -05:00
startTags = startTags [ : len ( startTags ) - 1 ]
2015-03-23 18:32:24 -04:00
default :
buf . WriteString ( token . String ( ) )
2015-03-08 22:14:50 -06:00
}
}
if io . EOF == tokenizer . Err ( ) {
return buf . Bytes ( )
}
2015-03-23 18:32:24 -04:00
// If we are not at the end of the input, then some other parsing error has occurred,
// so return the input verbatim.
2016-11-25 09:58:05 +08:00
return rawHTML
2015-03-08 22:14:50 -06:00
}
2015-03-23 18:32:24 -04:00
2017-02-14 08:13:59 +07:00
// Render renders Markdown to HTML with all specific handling stuff.
func render ( rawBytes [ ] byte , urlPrefix string , metas map [ string ] string , isWikiMarkdown bool ) [ ] byte {
urlPrefix = strings . Replace ( urlPrefix , " " , "%20" , - 1 )
result := RenderRaw ( rawBytes , urlPrefix , isWikiMarkdown )
2016-02-20 17:10:05 -05:00
result = PostProcess ( result , urlPrefix , metas )
2015-03-23 18:32:24 -04:00
result = Sanitizer . SanitizeBytes ( result )
return result
}
2017-02-14 08:13:59 +07:00
// Render renders Markdown to HTML with all specific handling stuff.
func Render ( rawBytes [ ] byte , urlPrefix string , metas map [ string ] string ) [ ] byte {
return render ( rawBytes , urlPrefix , metas , false )
}
2016-02-20 17:10:05 -05:00
// RenderString renders Markdown to HTML with special links and returns string type.
func RenderString ( raw , urlPrefix string , metas map [ string ] string ) string {
2017-02-14 08:13:59 +07:00
return string ( render ( [ ] byte ( raw ) , urlPrefix , metas , false ) )
}
// RenderWiki renders markdown wiki page to HTML and return HTML string
func RenderWiki ( rawBytes [ ] byte , urlPrefix string , metas map [ string ] string ) string {
return string ( render ( rawBytes , urlPrefix , metas , true ) )
2015-03-23 18:32:24 -04:00
}