forked from go-gitea/gitea
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhtml_node.go
More file actions
165 lines (147 loc) · 5 KB
/
html_node.go
File metadata and controls
165 lines (147 loc) · 5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
import (
"strings"
"code.gitea.io/gitea/modules/markup/common"
"golang.org/x/net/html"
)
func isAnchorIDUserContent(s string) bool {
// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
// old logic: blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-")
}
func isAnchorIDFootnote(s string) bool {
return strings.HasPrefix(s, "fnref:user-content-") || strings.HasPrefix(s, "fn:user-content-")
}
func isAnchorHrefFootnote(s string) bool {
return strings.HasPrefix(s, "#fnref:user-content-") || strings.HasPrefix(s, "#fn:user-content-")
}
// isHeadingTag returns true if the node is a heading tag (h1-h6)
func isHeadingTag(node *html.Node) bool {
return node.Type == html.ElementNode &&
len(node.Data) == 2 &&
node.Data[0] == 'h' &&
node.Data[1] >= '1' && node.Data[1] <= '6'
}
// getNodeText extracts the text content from a node and its children
func getNodeText(node *html.Node) string {
var text strings.Builder
var extractText func(*html.Node)
extractText = func(n *html.Node) {
if n.Type == html.TextNode {
text.WriteString(n.Data)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
extractText(c)
}
}
extractText(node)
return text.String()
}
func processNodeAttrID(node *html.Node) {
// Add user-content- to IDs and "#" links if they don't already have them,
// and convert the link href to a relative link to the host root
hasID := false
for idx, attr := range node.Attr {
if attr.Key == "id" {
hasID = true
if !isAnchorIDUserContent(attr.Val) {
node.Attr[idx].Val = "user-content-" + attr.Val
}
}
}
// For heading tags (h1-h6) without an id attribute, generate one from the text content
// This ensures HTML headings like <h1>Title</h1> get proper permalink anchors
// matching the behavior of Markdown headings
if !hasID && isHeadingTag(node) {
text := getNodeText(node)
if text != "" {
// Use the same CleanValue function used by Markdown heading ID generation
cleanedID := string(common.CleanValue([]byte(text)))
if cleanedID != "" {
node.Attr = append(node.Attr, html.Attribute{Key: "id", Val: "user-content-" + cleanedID})
}
}
}
}
func processFootnoteNode(ctx *RenderContext, node *html.Node) {
for idx, attr := range node.Attr {
if (attr.Key == "id" && isAnchorIDFootnote(attr.Val)) ||
(attr.Key == "href" && isAnchorHrefFootnote(attr.Val)) {
if footnoteContextID := ctx.RenderOptions.Metas["footnoteContextId"]; footnoteContextID != "" {
node.Attr[idx].Val = attr.Val + "-" + footnoteContextID
}
continue
}
}
}
func processNodeA(ctx *RenderContext, node *html.Node) {
for idx, attr := range node.Attr {
if attr.Key == "href" {
if anchorID, ok := strings.CutPrefix(attr.Val, "#"); ok {
if !isAnchorIDUserContent(attr.Val) {
node.Attr[idx].Val = "#user-content-" + anchorID
}
} else {
node.Attr[idx].Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeDefault)
}
}
}
}
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
next = img.NextSibling
attrSrc, hasLazy := "", false
for i, imgAttr := range img.Attr {
hasLazy = hasLazy || imgAttr.Key == "loading" && imgAttr.Val == "lazy"
if imgAttr.Key != "src" {
attrSrc = imgAttr.Val
continue
}
imgSrcOrigin := imgAttr.Val
isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:")
// By default, the "<img>" tag should also be clickable,
// because frontend uses `<img>` to paste the re-scaled image into the Markdown,
// so it must match the default Markdown image behavior.
cnt := 0
for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent {
if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
isLinkable = false
break
}
cnt++
}
if isLinkable {
wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
{Key: "target", Val: "_blank"},
}}
parent := img.Parent
imgNext := img.NextSibling
parent.RemoveChild(img)
parent.InsertBefore(wrapper, imgNext)
wrapper.AppendChild(img)
}
imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia)
imgAttr.Val = camoHandleLink(imgAttr.Val)
img.Attr[i] = imgAttr
}
if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") {
img.Attr = append(img.Attr, html.Attribute{Key: "loading", Val: "lazy"})
}
return next
}
func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
next = node.NextSibling
for i, attr := range node.Attr {
if attr.Key != "src" {
continue
}
if IsNonEmptyRelativePath(attr.Val) {
attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
}
attr.Val = camoHandleLink(attr.Val)
node.Attr[i] = attr
}
return next
}