You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

html.go 36KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "bytes"
  6. "io"
  7. "net/url"
  8. "path"
  9. "path/filepath"
  10. "regexp"
  11. "strings"
  12. "sync"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/emoji"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/markup/common"
  18. "code.gitea.io/gitea/modules/references"
  19. "code.gitea.io/gitea/modules/regexplru"
  20. "code.gitea.io/gitea/modules/setting"
  21. "code.gitea.io/gitea/modules/templates/vars"
  22. "code.gitea.io/gitea/modules/translation"
  23. "code.gitea.io/gitea/modules/util"
  24. "golang.org/x/net/html"
  25. "golang.org/x/net/html/atom"
  26. "mvdan.cc/xurls/v2"
  27. )
  28. // Issue name styles
  29. const (
  30. IssueNameStyleNumeric = "numeric"
  31. IssueNameStyleAlphanumeric = "alphanumeric"
  32. IssueNameStyleRegexp = "regexp"
  33. )
  34. var (
  35. // NOTE: All below regex matching do not perform any extra validation.
  36. // Thus a link is produced even if the linked entity does not exist.
  37. // While fast, this is also incorrect and lead to false positives.
  38. // TODO: fix invalid linking issue
  39. // valid chars in encoded path and parameter: [-+~_%.a-zA-Z0-9/]
  40. // sha1CurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
  41. // Although SHA1 hashes are 40 chars long, the regex matches the hash from 7 to 40 chars in length
  42. // so that abbreviated hash links can be used as well. This matches git and GitHub usability.
  43. sha1CurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,40})(?:\s|$|\)|\]|[.,](\s|$))`)
  44. // shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
  45. shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
  46. // anySHA1Pattern splits url containing SHA into parts
  47. anySHA1Pattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
  48. // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
  49. comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,40})(\.\.\.?)([0-9a-f]{7,40})?(#[-+~_%.a-zA-Z0-9]+)?`)
  50. validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`)
  51. // While this email regex is definitely not perfect and I'm sure you can come up
  52. // with edge cases, it is still accepted by the CommonMark specification, as
  53. // well as the HTML5 spec:
  54. // http://spec.commonmark.org/0.28/#email-address
  55. // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
  56. emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
  57. // blackfriday extensions create IDs like fn:user-content-footnote
  58. blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
  59. // EmojiShortCodeRegex find emoji by alias like :smile:
  60. EmojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
  61. )
  62. // CSS class for action keywords (e.g. "closes: #1")
  63. const keywordClass = "issue-keyword"
  64. // IsLink reports whether link fits valid format.
  65. func IsLink(link []byte) bool {
  66. return validLinksPattern.Match(link)
  67. }
  68. func IsLinkStr(link string) bool {
  69. return validLinksPattern.MatchString(link)
  70. }
  71. // regexp for full links to issues/pulls
  72. var issueFullPattern *regexp.Regexp
  73. // Once for to prevent races
  74. var issueFullPatternOnce sync.Once
  75. // regexp for full links to hash comment in pull request files changed tab
  76. var filesChangedFullPattern *regexp.Regexp
  77. // Once for to prevent races
  78. var filesChangedFullPatternOnce sync.Once
  79. func getIssueFullPattern() *regexp.Regexp {
  80. issueFullPatternOnce.Do(func() {
  81. // example: https://domain/org/repo/pulls/27#hash
  82. issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
  83. `[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
  84. })
  85. return issueFullPattern
  86. }
  87. func getFilesChangedFullPattern() *regexp.Regexp {
  88. filesChangedFullPatternOnce.Do(func() {
  89. // example: https://domain/org/repo/pulls/27/files#hash
  90. filesChangedFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
  91. `[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
  92. })
  93. return filesChangedFullPattern
  94. }
  95. // CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text
  96. func CustomLinkURLSchemes(schemes []string) {
  97. schemes = append(schemes, "http", "https")
  98. withAuth := make([]string, 0, len(schemes))
  99. validScheme := regexp.MustCompile(`^[a-z]+$`)
  100. for _, s := range schemes {
  101. if !validScheme.MatchString(s) {
  102. continue
  103. }
  104. without := false
  105. for _, sna := range xurls.SchemesNoAuthority {
  106. if s == sna {
  107. without = true
  108. break
  109. }
  110. }
  111. if without {
  112. s += ":"
  113. } else {
  114. s += "://"
  115. }
  116. withAuth = append(withAuth, s)
  117. }
  118. common.LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|"))
  119. }
  120. // IsSameDomain checks if given url string has the same hostname as current Gitea instance
  121. func IsSameDomain(s string) bool {
  122. if strings.HasPrefix(s, "/") {
  123. return true
  124. }
  125. if uapp, err := url.Parse(setting.AppURL); err == nil {
  126. if u, err := url.Parse(s); err == nil {
  127. return u.Host == uapp.Host
  128. }
  129. return false
  130. }
  131. return false
  132. }
  133. type postProcessError struct {
  134. context string
  135. err error
  136. }
  137. func (p *postProcessError) Error() string {
  138. return "PostProcess: " + p.context + ", " + p.err.Error()
  139. }
  140. type processor func(ctx *RenderContext, node *html.Node)
  141. var defaultProcessors = []processor{
  142. fullIssuePatternProcessor,
  143. comparePatternProcessor,
  144. fullSha1PatternProcessor,
  145. shortLinkProcessor,
  146. linkProcessor,
  147. mentionProcessor,
  148. issueIndexPatternProcessor,
  149. commitCrossReferencePatternProcessor,
  150. sha1CurrentPatternProcessor,
  151. emailAddressProcessor,
  152. emojiProcessor,
  153. emojiShortCodeProcessor,
  154. }
  155. // PostProcess does the final required transformations to the passed raw HTML
  156. // data, and ensures its validity. Transformations include: replacing links and
  157. // emails with HTML links, parsing shortlinks in the format of [[Link]], like
  158. // MediaWiki, linking issues in the format #ID, and mentions in the format
  159. // @user, and others.
  160. func PostProcess(
  161. ctx *RenderContext,
  162. input io.Reader,
  163. output io.Writer,
  164. ) error {
  165. return postProcess(ctx, defaultProcessors, input, output)
  166. }
  167. var commitMessageProcessors = []processor{
  168. fullIssuePatternProcessor,
  169. comparePatternProcessor,
  170. fullSha1PatternProcessor,
  171. linkProcessor,
  172. mentionProcessor,
  173. issueIndexPatternProcessor,
  174. commitCrossReferencePatternProcessor,
  175. sha1CurrentPatternProcessor,
  176. emailAddressProcessor,
  177. emojiProcessor,
  178. emojiShortCodeProcessor,
  179. }
  180. // RenderCommitMessage will use the same logic as PostProcess, but will disable
  181. // the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
  182. // set, which changes every text node into a link to the passed default link.
  183. func RenderCommitMessage(
  184. ctx *RenderContext,
  185. content string,
  186. ) (string, error) {
  187. procs := commitMessageProcessors
  188. if ctx.DefaultLink != "" {
  189. // we don't have to fear data races, because being
  190. // commitMessageProcessors of fixed len and cap, every time we append
  191. // something to it the slice is realloc+copied, so append always
  192. // generates the slice ex-novo.
  193. procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink))
  194. }
  195. return renderProcessString(ctx, procs, content)
  196. }
  197. var commitMessageSubjectProcessors = []processor{
  198. fullIssuePatternProcessor,
  199. comparePatternProcessor,
  200. fullSha1PatternProcessor,
  201. linkProcessor,
  202. mentionProcessor,
  203. issueIndexPatternProcessor,
  204. commitCrossReferencePatternProcessor,
  205. sha1CurrentPatternProcessor,
  206. emojiShortCodeProcessor,
  207. emojiProcessor,
  208. }
  209. var emojiProcessors = []processor{
  210. emojiShortCodeProcessor,
  211. emojiProcessor,
  212. }
  213. // RenderCommitMessageSubject will use the same logic as PostProcess and
  214. // RenderCommitMessage, but will disable the shortLinkProcessor and
  215. // emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
  216. // which changes every text node into a link to the passed default link.
  217. func RenderCommitMessageSubject(
  218. ctx *RenderContext,
  219. content string,
  220. ) (string, error) {
  221. procs := commitMessageSubjectProcessors
  222. if ctx.DefaultLink != "" {
  223. // we don't have to fear data races, because being
  224. // commitMessageSubjectProcessors of fixed len and cap, every time we
  225. // append something to it the slice is realloc+copied, so append always
  226. // generates the slice ex-novo.
  227. procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink))
  228. }
  229. return renderProcessString(ctx, procs, content)
  230. }
  231. // RenderIssueTitle to process title on individual issue/pull page
  232. func RenderIssueTitle(
  233. ctx *RenderContext,
  234. title string,
  235. ) (string, error) {
  236. return renderProcessString(ctx, []processor{
  237. issueIndexPatternProcessor,
  238. commitCrossReferencePatternProcessor,
  239. sha1CurrentPatternProcessor,
  240. emojiShortCodeProcessor,
  241. emojiProcessor,
  242. }, title)
  243. }
  244. func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
  245. var buf strings.Builder
  246. if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
  247. return "", err
  248. }
  249. return buf.String(), nil
  250. }
  251. // RenderDescriptionHTML will use similar logic as PostProcess, but will
  252. // use a single special linkProcessor.
  253. func RenderDescriptionHTML(
  254. ctx *RenderContext,
  255. content string,
  256. ) (string, error) {
  257. return renderProcessString(ctx, []processor{
  258. descriptionLinkProcessor,
  259. emojiShortCodeProcessor,
  260. emojiProcessor,
  261. }, content)
  262. }
  263. // RenderEmoji for when we want to just process emoji and shortcodes
  264. // in various places it isn't already run through the normal markdown processor
  265. func RenderEmoji(
  266. ctx *RenderContext,
  267. content string,
  268. ) (string, error) {
  269. return renderProcessString(ctx, emojiProcessors, content)
  270. }
  271. var (
  272. tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`)
  273. nulCleaner = strings.NewReplacer("\000", "")
  274. )
  275. func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
  276. defer ctx.Cancel()
  277. // FIXME: don't read all content to memory
  278. rawHTML, err := io.ReadAll(input)
  279. if err != nil {
  280. return err
  281. }
  282. // parse the HTML
  283. node, err := html.Parse(io.MultiReader(
  284. // prepend "<html><body>"
  285. strings.NewReader("<html><body>"),
  286. // Strip out nuls - they're always invalid
  287. bytes.NewReader(tagCleaner.ReplaceAll([]byte(nulCleaner.Replace(string(rawHTML))), []byte("&lt;$1"))),
  288. // close the tags
  289. strings.NewReader("</body></html>"),
  290. ))
  291. if err != nil {
  292. return &postProcessError{"invalid HTML", err}
  293. }
  294. if node.Type == html.DocumentNode {
  295. node = node.FirstChild
  296. }
  297. visitNode(ctx, procs, node)
  298. newNodes := make([]*html.Node, 0, 5)
  299. if node.Data == "html" {
  300. node = node.FirstChild
  301. for node != nil && node.Data != "body" {
  302. node = node.NextSibling
  303. }
  304. }
  305. if node != nil {
  306. if node.Data == "body" {
  307. child := node.FirstChild
  308. for child != nil {
  309. newNodes = append(newNodes, child)
  310. child = child.NextSibling
  311. }
  312. } else {
  313. newNodes = append(newNodes, node)
  314. }
  315. }
  316. // Render everything to buf.
  317. for _, node := range newNodes {
  318. if err := html.Render(output, node); err != nil {
  319. return &postProcessError{"error rendering processed HTML", err}
  320. }
  321. }
  322. return nil
  323. }
  324. func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
  325. // Add user-content- to IDs and "#" links if they don't already have them
  326. for idx, attr := range node.Attr {
  327. val := strings.TrimPrefix(attr.Val, "#")
  328. notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val))
  329. if attr.Key == "id" && notHasPrefix {
  330. node.Attr[idx].Val = "user-content-" + attr.Val
  331. }
  332. if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix {
  333. node.Attr[idx].Val = "#user-content-" + val
  334. }
  335. if attr.Key == "class" && attr.Val == "emoji" {
  336. procs = nil
  337. }
  338. }
  339. // We ignore code and pre.
  340. switch node.Type {
  341. case html.TextNode:
  342. textNode(ctx, procs, node)
  343. case html.ElementNode:
  344. if node.Data == "img" {
  345. for i, attr := range node.Attr {
  346. if attr.Key != "src" {
  347. continue
  348. }
  349. if len(attr.Val) > 0 && !IsLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
  350. attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
  351. }
  352. attr.Val = camoHandleLink(attr.Val)
  353. node.Attr[i] = attr
  354. }
  355. } else if node.Data == "a" {
  356. // Restrict text in links to emojis
  357. procs = emojiProcessors
  358. } else if node.Data == "code" || node.Data == "pre" {
  359. return
  360. } else if node.Data == "i" {
  361. for _, attr := range node.Attr {
  362. if attr.Key != "class" {
  363. continue
  364. }
  365. classes := strings.Split(attr.Val, " ")
  366. for i, class := range classes {
  367. if class == "icon" {
  368. classes[0], classes[i] = classes[i], classes[0]
  369. attr.Val = strings.Join(classes, " ")
  370. // Remove all children of icons
  371. child := node.FirstChild
  372. for child != nil {
  373. node.RemoveChild(child)
  374. child = node.FirstChild
  375. }
  376. break
  377. }
  378. }
  379. }
  380. }
  381. for n := node.FirstChild; n != nil; n = n.NextSibling {
  382. visitNode(ctx, procs, n)
  383. }
  384. }
  385. // ignore everything else
  386. }
  387. // textNode runs the passed node through various processors, in order to handle
  388. // all kinds of special links handled by the post-processing.
  389. func textNode(ctx *RenderContext, procs []processor, node *html.Node) {
  390. for _, processor := range procs {
  391. processor(ctx, node)
  392. }
  393. }
  394. // createKeyword() renders a highlighted version of an action keyword
  395. func createKeyword(content string) *html.Node {
  396. span := &html.Node{
  397. Type: html.ElementNode,
  398. Data: atom.Span.String(),
  399. Attr: []html.Attribute{},
  400. }
  401. span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass})
  402. text := &html.Node{
  403. Type: html.TextNode,
  404. Data: content,
  405. }
  406. span.AppendChild(text)
  407. return span
  408. }
  409. func createEmoji(content, class, name string) *html.Node {
  410. span := &html.Node{
  411. Type: html.ElementNode,
  412. Data: atom.Span.String(),
  413. Attr: []html.Attribute{},
  414. }
  415. if class != "" {
  416. span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: class})
  417. }
  418. if name != "" {
  419. span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: name})
  420. }
  421. text := &html.Node{
  422. Type: html.TextNode,
  423. Data: content,
  424. }
  425. span.AppendChild(text)
  426. return span
  427. }
  428. func createCustomEmoji(alias string) *html.Node {
  429. span := &html.Node{
  430. Type: html.ElementNode,
  431. Data: atom.Span.String(),
  432. Attr: []html.Attribute{},
  433. }
  434. span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: "emoji"})
  435. span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
  436. img := &html.Node{
  437. Type: html.ElementNode,
  438. DataAtom: atom.Img,
  439. Data: "img",
  440. Attr: []html.Attribute{},
  441. }
  442. img.Attr = append(img.Attr, html.Attribute{Key: "alt", Val: ":" + alias + ":"})
  443. img.Attr = append(img.Attr, html.Attribute{Key: "src", Val: setting.StaticURLPrefix + "/assets/img/emoji/" + alias + ".png"})
  444. span.AppendChild(img)
  445. return span
  446. }
  447. func createLink(href, content, class string) *html.Node {
  448. a := &html.Node{
  449. Type: html.ElementNode,
  450. Data: atom.A.String(),
  451. Attr: []html.Attribute{{Key: "href", Val: href}},
  452. }
  453. if class != "" {
  454. a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
  455. }
  456. text := &html.Node{
  457. Type: html.TextNode,
  458. Data: content,
  459. }
  460. a.AppendChild(text)
  461. return a
  462. }
  463. func createCodeLink(href, content, class string) *html.Node {
  464. a := &html.Node{
  465. Type: html.ElementNode,
  466. Data: atom.A.String(),
  467. Attr: []html.Attribute{{Key: "href", Val: href}},
  468. }
  469. if class != "" {
  470. a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
  471. }
  472. text := &html.Node{
  473. Type: html.TextNode,
  474. Data: content,
  475. }
  476. code := &html.Node{
  477. Type: html.ElementNode,
  478. Data: atom.Code.String(),
  479. Attr: []html.Attribute{{Key: "class", Val: "nohighlight"}},
  480. }
  481. code.AppendChild(text)
  482. a.AppendChild(code)
  483. return a
  484. }
  485. // replaceContent takes text node, and in its content it replaces a section of
  486. // it with the specified newNode.
  487. func replaceContent(node *html.Node, i, j int, newNode *html.Node) {
  488. replaceContentList(node, i, j, []*html.Node{newNode})
  489. }
  490. // replaceContentList takes text node, and in its content it replaces a section of
  491. // it with the specified newNodes. An example to visualize how this can work can
  492. // be found here: https://play.golang.org/p/5zP8NnHZ03s
  493. func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
  494. // get the data before and after the match
  495. before := node.Data[:i]
  496. after := node.Data[j:]
  497. // Replace in the current node the text, so that it is only what it is
  498. // supposed to have.
  499. node.Data = before
  500. // Get the current next sibling, before which we place the replaced data,
  501. // and after that we place the new text node.
  502. nextSibling := node.NextSibling
  503. for _, n := range newNodes {
  504. node.Parent.InsertBefore(n, nextSibling)
  505. }
  506. if after != "" {
  507. node.Parent.InsertBefore(&html.Node{
  508. Type: html.TextNode,
  509. Data: after,
  510. }, nextSibling)
  511. }
  512. }
  513. func mentionProcessor(ctx *RenderContext, node *html.Node) {
  514. start := 0
  515. next := node.NextSibling
  516. for node != nil && node != next && start < len(node.Data) {
  517. // We replace only the first mention; other mentions will be addressed later
  518. found, loc := references.FindFirstMentionBytes([]byte(node.Data[start:]))
  519. if !found {
  520. return
  521. }
  522. loc.Start += start
  523. loc.End += start
  524. mention := node.Data[loc.Start:loc.End]
  525. var teams string
  526. teams, ok := ctx.Metas["teams"]
  527. // FIXME: util.URLJoin may not be necessary here:
  528. // - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
  529. // is an AppSubURL link we can probably fallback to concatenation.
  530. // team mention should follow @orgName/teamName style
  531. if ok && strings.Contains(mention, "/") {
  532. mentionOrgAndTeam := strings.Split(mention, "/")
  533. if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
  534. replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
  535. node = node.NextSibling.NextSibling
  536. start = 0
  537. continue
  538. }
  539. start = loc.End
  540. continue
  541. }
  542. mentionedUsername := mention[1:]
  543. if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
  544. replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention"))
  545. node = node.NextSibling.NextSibling
  546. } else {
  547. node = node.NextSibling
  548. }
  549. start = 0
  550. }
  551. }
  552. func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
  553. next := node.NextSibling
  554. for node != nil && node != next {
  555. m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
  556. if m == nil {
  557. return
  558. }
  559. content := node.Data[m[2]:m[3]]
  560. tail := node.Data[m[4]:m[5]]
  561. props := make(map[string]string)
  562. // MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
  563. // It makes page handling terrible, but we prefer GitHub syntax
  564. // And fall back to MediaWiki only when it is obvious from the look
  565. // Of text and link contents
  566. sl := strings.Split(content, "|")
  567. for _, v := range sl {
  568. if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
  569. // There is no equal in this argument; this is a mandatory arg
  570. if props["name"] == "" {
  571. if IsLinkStr(v) {
  572. // If we clearly see it is a link, we save it so
  573. // But first we need to ensure, that if both mandatory args provided
  574. // look like links, we stick to GitHub syntax
  575. if props["link"] != "" {
  576. props["name"] = props["link"]
  577. }
  578. props["link"] = strings.TrimSpace(v)
  579. } else {
  580. props["name"] = v
  581. }
  582. } else {
  583. props["link"] = strings.TrimSpace(v)
  584. }
  585. } else {
  586. // There is an equal; optional argument.
  587. sep := strings.IndexByte(v, '=')
  588. key, val := v[:sep], html.UnescapeString(v[sep+1:])
  589. // When parsing HTML, x/net/html will change all quotes which are
  590. // not used for syntax into UTF-8 quotes. So checking val[0] won't
  591. // be enough, since that only checks a single byte.
  592. if len(val) > 1 {
  593. if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) ||
  594. (strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) {
  595. const lenQuote = len("‘")
  596. val = val[lenQuote : len(val)-lenQuote]
  597. } else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) ||
  598. (strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) {
  599. val = val[1 : len(val)-1]
  600. } else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") {
  601. const lenQuote = len("‘")
  602. val = val[1 : len(val)-lenQuote]
  603. }
  604. }
  605. props[key] = val
  606. }
  607. }
  608. var name, link string
  609. if props["link"] != "" {
  610. link = props["link"]
  611. } else if props["name"] != "" {
  612. link = props["name"]
  613. }
  614. if props["title"] != "" {
  615. name = props["title"]
  616. } else if props["name"] != "" {
  617. name = props["name"]
  618. } else {
  619. name = link
  620. }
  621. name += tail
  622. image := false
  623. switch ext := filepath.Ext(link); ext {
  624. // fast path: empty string, ignore
  625. case "":
  626. // leave image as false
  627. case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
  628. image = true
  629. }
  630. childNode := &html.Node{}
  631. linkNode := &html.Node{
  632. FirstChild: childNode,
  633. LastChild: childNode,
  634. Type: html.ElementNode,
  635. Data: "a",
  636. DataAtom: atom.A,
  637. }
  638. childNode.Parent = linkNode
  639. absoluteLink := IsLinkStr(link)
  640. if !absoluteLink {
  641. if image {
  642. link = strings.ReplaceAll(link, " ", "+")
  643. } else {
  644. link = strings.ReplaceAll(link, " ", "-")
  645. }
  646. if !strings.Contains(link, "/") {
  647. link = url.PathEscape(link)
  648. }
  649. }
  650. if image {
  651. if !absoluteLink {
  652. link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), link)
  653. }
  654. title := props["title"]
  655. if title == "" {
  656. title = props["alt"]
  657. }
  658. if title == "" {
  659. title = path.Base(name)
  660. }
  661. alt := props["alt"]
  662. if alt == "" {
  663. alt = name
  664. }
  665. // make the childNode an image - if we can, we also place the alt
  666. childNode.Type = html.ElementNode
  667. childNode.Data = "img"
  668. childNode.DataAtom = atom.Img
  669. childNode.Attr = []html.Attribute{
  670. {Key: "src", Val: link},
  671. {Key: "title", Val: title},
  672. {Key: "alt", Val: alt},
  673. }
  674. if alt == "" {
  675. childNode.Attr = childNode.Attr[:2]
  676. }
  677. } else {
  678. if !absoluteLink {
  679. if ctx.IsWiki {
  680. link = util.URLJoin(ctx.Links.WikiLink(), link)
  681. } else {
  682. link = util.URLJoin(ctx.Links.SrcLink(), link)
  683. }
  684. }
  685. childNode.Type = html.TextNode
  686. childNode.Data = name
  687. }
  688. linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
  689. replaceContent(node, m[0], m[1], linkNode)
  690. node = node.NextSibling.NextSibling
  691. }
  692. }
  693. func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
  694. if ctx.Metas == nil {
  695. return
  696. }
  697. next := node.NextSibling
  698. for node != nil && node != next {
  699. m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
  700. if m == nil {
  701. return
  702. }
  703. mDiffView := getFilesChangedFullPattern().FindStringSubmatchIndex(node.Data)
  704. // leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files
  705. if mDiffView != nil {
  706. return
  707. }
  708. link := node.Data[m[0]:m[1]]
  709. text := "#" + node.Data[m[2]:m[3]]
  710. // if m[4] and m[5] is not -1, then link is to a comment
  711. // indicate that in the text by appending (comment)
  712. if m[4] != -1 && m[5] != -1 {
  713. if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
  714. text += " " + locale.Tr("repo.from_comment")
  715. } else {
  716. text += " (comment)"
  717. }
  718. }
  719. // extract repo and org name from matched link like
  720. // http://localhost:3000/gituser/myrepo/issues/1
  721. linkParts := strings.Split(link, "/")
  722. matchOrg := linkParts[len(linkParts)-4]
  723. matchRepo := linkParts[len(linkParts)-3]
  724. if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] {
  725. replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
  726. } else {
  727. text = matchOrg + "/" + matchRepo + text
  728. replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
  729. }
  730. node = node.NextSibling.NextSibling
  731. }
  732. }
  733. func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
  734. // FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
  735. // The "mode" approach should be refactored to some other more clear&reliable way.
  736. if ctx.Metas == nil || (ctx.Metas["mode"] == "document" && !ctx.IsWiki) {
  737. return
  738. }
  739. var (
  740. found bool
  741. ref *references.RenderizableReference
  742. )
  743. next := node.NextSibling
  744. for node != nil && node != next {
  745. _, hasExtTrackFormat := ctx.Metas["format"]
  746. // Repos with external issue trackers might still need to reference local PRs
  747. // We need to concern with the first one that shows up in the text, whichever it is
  748. isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
  749. foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle)
  750. switch ctx.Metas["style"] {
  751. case "", IssueNameStyleNumeric:
  752. found, ref = foundNumeric, refNumeric
  753. case IssueNameStyleAlphanumeric:
  754. found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
  755. case IssueNameStyleRegexp:
  756. pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"])
  757. if err != nil {
  758. return
  759. }
  760. found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
  761. }
  762. // Repos with external issue trackers might still need to reference local PRs
  763. // We need to concern with the first one that shows up in the text, whichever it is
  764. if hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
  765. // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
  766. // Allow a free-pass when non-numeric pattern wasn't found.
  767. if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) {
  768. found = foundNumeric
  769. ref = refNumeric
  770. }
  771. }
  772. if !found {
  773. return
  774. }
  775. var link *html.Node
  776. reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
  777. if hasExtTrackFormat && !ref.IsPull {
  778. ctx.Metas["index"] = ref.Issue
  779. res, err := vars.Expand(ctx.Metas["format"], ctx.Metas)
  780. if err != nil {
  781. // here we could just log the error and continue the rendering
  782. log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
  783. }
  784. link = createLink(res, reftext, "ref-issue ref-external-issue")
  785. } else {
  786. // Path determines the type of link that will be rendered. It's unknown at this point whether
  787. // the linked item is actually a PR or an issue. Luckily it's of no real consequence because
  788. // Gitea will redirect on click as appropriate.
  789. path := "issues"
  790. if ref.IsPull {
  791. path = "pulls"
  792. }
  793. if ref.Owner == "" {
  794. link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue")
  795. } else {
  796. link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
  797. }
  798. }
  799. if ref.Action == references.XRefActionNone {
  800. replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
  801. node = node.NextSibling.NextSibling
  802. continue
  803. }
  804. // Decorate action keywords if actionable
  805. var keyword *html.Node
  806. if references.IsXrefActionable(ref, hasExtTrackFormat) {
  807. keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
  808. } else {
  809. keyword = &html.Node{
  810. Type: html.TextNode,
  811. Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End],
  812. }
  813. }
  814. spaces := &html.Node{
  815. Type: html.TextNode,
  816. Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
  817. }
  818. replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link})
  819. node = node.NextSibling.NextSibling.NextSibling.NextSibling
  820. }
  821. }
  822. func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
  823. next := node.NextSibling
  824. for node != nil && node != next {
  825. found, ref := references.FindRenderizableCommitCrossReference(node.Data)
  826. if !found {
  827. return
  828. }
  829. reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
  830. link := createLink(util.URLJoin(setting.AppSubURL, ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
  831. replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
  832. node = node.NextSibling.NextSibling
  833. }
  834. }
  835. // fullSha1PatternProcessor renders SHA containing URLs
  836. func fullSha1PatternProcessor(ctx *RenderContext, node *html.Node) {
  837. if ctx.Metas == nil {
  838. return
  839. }
  840. next := node.NextSibling
  841. for node != nil && node != next {
  842. m := anySHA1Pattern.FindStringSubmatchIndex(node.Data)
  843. if m == nil {
  844. return
  845. }
  846. urlFull := node.Data[m[0]:m[1]]
  847. text := base.ShortSha(node.Data[m[2]:m[3]])
  848. // 3rd capture group matches a optional path
  849. subpath := ""
  850. if m[5] > 0 {
  851. subpath = node.Data[m[4]:m[5]]
  852. }
  853. // 4th capture group matches a optional url hash
  854. hash := ""
  855. if m[7] > 0 {
  856. hash = node.Data[m[6]:m[7]][1:]
  857. }
  858. start := m[0]
  859. end := m[1]
  860. // If url ends in '.', it's very likely that it is not part of the
  861. // actual url but used to finish a sentence.
  862. if strings.HasSuffix(urlFull, ".") {
  863. end--
  864. urlFull = urlFull[:len(urlFull)-1]
  865. if hash != "" {
  866. hash = hash[:len(hash)-1]
  867. } else if subpath != "" {
  868. subpath = subpath[:len(subpath)-1]
  869. }
  870. }
  871. if subpath != "" {
  872. text += subpath
  873. }
  874. if hash != "" {
  875. text += " (" + hash + ")"
  876. }
  877. replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
  878. node = node.NextSibling.NextSibling
  879. }
  880. }
  881. func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
  882. if ctx.Metas == nil {
  883. return
  884. }
  885. next := node.NextSibling
  886. for node != nil && node != next {
  887. m := comparePattern.FindStringSubmatchIndex(node.Data)
  888. if m == nil {
  889. return
  890. }
  891. // Ensure that every group (m[0]...m[7]) has a match
  892. for i := 0; i < 8; i++ {
  893. if m[i] == -1 {
  894. return
  895. }
  896. }
  897. urlFull := node.Data[m[0]:m[1]]
  898. text1 := base.ShortSha(node.Data[m[2]:m[3]])
  899. textDots := base.ShortSha(node.Data[m[4]:m[5]])
  900. text2 := base.ShortSha(node.Data[m[6]:m[7]])
  901. hash := ""
  902. if m[9] > 0 {
  903. hash = node.Data[m[8]:m[9]][1:]
  904. }
  905. start := m[0]
  906. end := m[1]
  907. // If url ends in '.', it's very likely that it is not part of the
  908. // actual url but used to finish a sentence.
  909. if strings.HasSuffix(urlFull, ".") {
  910. end--
  911. urlFull = urlFull[:len(urlFull)-1]
  912. if hash != "" {
  913. hash = hash[:len(hash)-1]
  914. } else if text2 != "" {
  915. text2 = text2[:len(text2)-1]
  916. }
  917. }
  918. text := text1 + textDots + text2
  919. if hash != "" {
  920. text += " (" + hash + ")"
  921. }
  922. replaceContent(node, start, end, createCodeLink(urlFull, text, "compare"))
  923. node = node.NextSibling.NextSibling
  924. }
  925. }
  926. // emojiShortCodeProcessor for rendering text like :smile: into emoji
  927. func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
  928. start := 0
  929. next := node.NextSibling
  930. for node != nil && node != next && start < len(node.Data) {
  931. m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
  932. if m == nil {
  933. return
  934. }
  935. m[0] += start
  936. m[1] += start
  937. start = m[1]
  938. alias := node.Data[m[0]:m[1]]
  939. alias = strings.ReplaceAll(alias, ":", "")
  940. converted := emoji.FromAlias(alias)
  941. if converted == nil {
  942. // check if this is a custom reaction
  943. if _, exist := setting.UI.CustomEmojisMap[alias]; exist {
  944. replaceContent(node, m[0], m[1], createCustomEmoji(alias))
  945. node = node.NextSibling.NextSibling
  946. start = 0
  947. continue
  948. }
  949. continue
  950. }
  951. replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description))
  952. node = node.NextSibling.NextSibling
  953. start = 0
  954. }
  955. }
  956. // emoji processor to match emoji and add emoji class
  957. func emojiProcessor(ctx *RenderContext, node *html.Node) {
  958. start := 0
  959. next := node.NextSibling
  960. for node != nil && node != next && start < len(node.Data) {
  961. m := emoji.FindEmojiSubmatchIndex(node.Data[start:])
  962. if m == nil {
  963. return
  964. }
  965. m[0] += start
  966. m[1] += start
  967. codepoint := node.Data[m[0]:m[1]]
  968. start = m[1]
  969. val := emoji.FromCode(codepoint)
  970. if val != nil {
  971. replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description))
  972. node = node.NextSibling.NextSibling
  973. start = 0
  974. }
  975. }
  976. }
  977. // sha1CurrentPatternProcessor renders SHA1 strings to corresponding links that
  978. // are assumed to be in the same repository.
  979. func sha1CurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
  980. if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" {
  981. return
  982. }
  983. start := 0
  984. next := node.NextSibling
  985. if ctx.ShaExistCache == nil {
  986. ctx.ShaExistCache = make(map[string]bool)
  987. }
  988. for node != nil && node != next && start < len(node.Data) {
  989. m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data[start:])
  990. if m == nil {
  991. return
  992. }
  993. m[2] += start
  994. m[3] += start
  995. hash := node.Data[m[2]:m[3]]
  996. // The regex does not lie, it matches the hash pattern.
  997. // However, a regex cannot know if a hash actually exists or not.
  998. // We could assume that a SHA1 hash should probably contain alphas AND numerics
  999. // but that is not always the case.
  1000. // Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
  1001. // as used by git and github for linking and thus we have to do similar.
  1002. // Because of this, we check to make sure that a matched hash is actually
  1003. // a commit in the repository before making it a link.
  1004. // check cache first
  1005. exist, inCache := ctx.ShaExistCache[hash]
  1006. if !inCache {
  1007. if ctx.GitRepo == nil {
  1008. var err error
  1009. ctx.GitRepo, err = git.OpenRepository(ctx.Ctx, ctx.Metas["repoPath"])
  1010. if err != nil {
  1011. log.Error("unable to open repository: %s Error: %v", ctx.Metas["repoPath"], err)
  1012. return
  1013. }
  1014. ctx.AddCancel(func() {
  1015. ctx.GitRepo.Close()
  1016. ctx.GitRepo = nil
  1017. })
  1018. }
  1019. exist = ctx.GitRepo.IsObjectExist(hash)
  1020. ctx.ShaExistCache[hash] = exist
  1021. }
  1022. if !exist {
  1023. start = m[3]
  1024. continue
  1025. }
  1026. link := util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
  1027. replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
  1028. start = 0
  1029. node = node.NextSibling.NextSibling
  1030. }
  1031. }
  1032. // emailAddressProcessor replaces raw email addresses with a mailto: link.
  1033. func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
  1034. next := node.NextSibling
  1035. for node != nil && node != next {
  1036. m := emailRegex.FindStringSubmatchIndex(node.Data)
  1037. if m == nil {
  1038. return
  1039. }
  1040. mail := node.Data[m[2]:m[3]]
  1041. replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto"))
  1042. node = node.NextSibling.NextSibling
  1043. }
  1044. }
  1045. // linkProcessor creates links for any HTTP or HTTPS URL not captured by
  1046. // markdown.
  1047. func linkProcessor(ctx *RenderContext, node *html.Node) {
  1048. next := node.NextSibling
  1049. for node != nil && node != next {
  1050. m := common.LinkRegex.FindStringIndex(node.Data)
  1051. if m == nil {
  1052. return
  1053. }
  1054. uri := node.Data[m[0]:m[1]]
  1055. replaceContent(node, m[0], m[1], createLink(uri, uri, "link"))
  1056. node = node.NextSibling.NextSibling
  1057. }
  1058. }
  1059. func genDefaultLinkProcessor(defaultLink string) processor {
  1060. return func(ctx *RenderContext, node *html.Node) {
  1061. ch := &html.Node{
  1062. Parent: node,
  1063. Type: html.TextNode,
  1064. Data: node.Data,
  1065. }
  1066. node.Type = html.ElementNode
  1067. node.Data = "a"
  1068. node.DataAtom = atom.A
  1069. node.Attr = []html.Attribute{
  1070. {Key: "href", Val: defaultLink},
  1071. {Key: "class", Val: "default-link muted"},
  1072. }
  1073. node.FirstChild, node.LastChild = ch, ch
  1074. }
  1075. }
  1076. // descriptionLinkProcessor creates links for DescriptionHTML
  1077. func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) {
  1078. next := node.NextSibling
  1079. for node != nil && node != next {
  1080. m := common.LinkRegex.FindStringIndex(node.Data)
  1081. if m == nil {
  1082. return
  1083. }
  1084. uri := node.Data[m[0]:m[1]]
  1085. replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri))
  1086. node = node.NextSibling.NextSibling
  1087. }
  1088. }
  1089. func createDescriptionLink(href, content string) *html.Node {
  1090. textNode := &html.Node{
  1091. Type: html.TextNode,
  1092. Data: content,
  1093. }
  1094. linkNode := &html.Node{
  1095. FirstChild: textNode,
  1096. LastChild: textNode,
  1097. Type: html.ElementNode,
  1098. Data: "a",
  1099. DataAtom: atom.A,
  1100. Attr: []html.Attribute{
  1101. {Key: "href", Val: href},
  1102. {Key: "target", Val: "_blank"},
  1103. {Key: "rel", Val: "noopener noreferrer"},
  1104. },
  1105. }
  1106. textNode.Parent = linkNode
  1107. return linkNode
  1108. }