feat(ui): add more emoji and code block rendering in issues
This commit is contained in:
		
					parent
					
						
							
								60bcdc8bc3
							
						
					
				
			
			
				commit
				
					
						4a74113dee
					
				
			
		
					 14 changed files with 322 additions and 34 deletions
				
			
		| 
						 | 
					@ -73,6 +73,8 @@ var (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// EmojiShortCodeRegex find emoji by alias like :smile:
 | 
						// EmojiShortCodeRegex find emoji by alias like :smile:
 | 
				
			||||||
	EmojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
 | 
						EmojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						InlineCodeBlockRegex = regexp.MustCompile("`[^`]+`")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CSS class for action keywords (e.g. "closes: #1")
 | 
					// CSS class for action keywords (e.g. "closes: #1")
 | 
				
			||||||
| 
						 | 
					@ -243,6 +245,7 @@ func RenderIssueTitle(
 | 
				
			||||||
	title string,
 | 
						title string,
 | 
				
			||||||
) (string, error) {
 | 
					) (string, error) {
 | 
				
			||||||
	return renderProcessString(ctx, []processor{
 | 
						return renderProcessString(ctx, []processor{
 | 
				
			||||||
 | 
							inlineCodeBlockProcessor,
 | 
				
			||||||
		issueIndexPatternProcessor,
 | 
							issueIndexPatternProcessor,
 | 
				
			||||||
		commitCrossReferencePatternProcessor,
 | 
							commitCrossReferencePatternProcessor,
 | 
				
			||||||
		hashCurrentPatternProcessor,
 | 
							hashCurrentPatternProcessor,
 | 
				
			||||||
| 
						 | 
					@ -251,6 +254,19 @@ func RenderIssueTitle(
 | 
				
			||||||
	}, title)
 | 
						}, title)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RenderRefIssueTitle to process title on places where an issue is referenced
 | 
				
			||||||
 | 
					func RenderRefIssueTitle(
 | 
				
			||||||
 | 
						ctx *RenderContext,
 | 
				
			||||||
 | 
						title string,
 | 
				
			||||||
 | 
					) (string, error) {
 | 
				
			||||||
 | 
						return renderProcessString(ctx, []processor{
 | 
				
			||||||
 | 
							inlineCodeBlockProcessor,
 | 
				
			||||||
 | 
							issueIndexPatternProcessor,
 | 
				
			||||||
 | 
							emojiShortCodeProcessor,
 | 
				
			||||||
 | 
							emojiProcessor,
 | 
				
			||||||
 | 
						}, title)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
 | 
					func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
 | 
				
			||||||
	var buf strings.Builder
 | 
						var buf strings.Builder
 | 
				
			||||||
	if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
 | 
						if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
 | 
				
			||||||
| 
						 | 
					@ -438,6 +454,24 @@ func createKeyword(content string) *html.Node {
 | 
				
			||||||
	return span
 | 
						return span
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createInlineCode(content string) *html.Node {
 | 
				
			||||||
 | 
						code := &html.Node{
 | 
				
			||||||
 | 
							Type: html.ElementNode,
 | 
				
			||||||
 | 
							Data: atom.Code.String(),
 | 
				
			||||||
 | 
							Attr: []html.Attribute{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						code.Attr = append(code.Attr, html.Attribute{Key: "class", Val: "inline-code-block"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						text := &html.Node{
 | 
				
			||||||
 | 
							Type: html.TextNode,
 | 
				
			||||||
 | 
							Data: content,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						code.AppendChild(text)
 | 
				
			||||||
 | 
						return code
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func createEmoji(content, class, name string) *html.Node {
 | 
					func createEmoji(content, class, name string) *html.Node {
 | 
				
			||||||
	span := &html.Node{
 | 
						span := &html.Node{
 | 
				
			||||||
		Type: html.ElementNode,
 | 
							Type: html.ElementNode,
 | 
				
			||||||
| 
						 | 
					@ -1070,6 +1104,21 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func inlineCodeBlockProcessor(ctx *RenderContext, node *html.Node) {
 | 
				
			||||||
 | 
						start := 0
 | 
				
			||||||
 | 
						next := node.NextSibling
 | 
				
			||||||
 | 
						for node != nil && node != next && start < len(node.Data) {
 | 
				
			||||||
 | 
							m := InlineCodeBlockRegex.FindStringSubmatchIndex(node.Data[start:])
 | 
				
			||||||
 | 
							if m == nil {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							code := node.Data[m[0]+1 : m[1]-1]
 | 
				
			||||||
 | 
							replaceContent(node, m[0], m[1], createInlineCode(code))
 | 
				
			||||||
 | 
							node = node.NextSibling.NextSibling
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// emojiShortCodeProcessor for rendering text like :smile: into emoji
 | 
					// emojiShortCodeProcessor for rendering text like :smile: into emoji
 | 
				
			||||||
func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
 | 
					func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
 | 
				
			||||||
	start := 0
 | 
						start := 0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -175,6 +175,7 @@ func NewFuncMap() template.FuncMap {
 | 
				
			||||||
		"RenderCommitBody":    RenderCommitBody,
 | 
							"RenderCommitBody":    RenderCommitBody,
 | 
				
			||||||
		"RenderCodeBlock":     RenderCodeBlock,
 | 
							"RenderCodeBlock":     RenderCodeBlock,
 | 
				
			||||||
		"RenderIssueTitle":    RenderIssueTitle,
 | 
							"RenderIssueTitle":    RenderIssueTitle,
 | 
				
			||||||
 | 
							"RenderRefIssueTitle": RenderRefIssueTitle,
 | 
				
			||||||
		"RenderEmoji":         RenderEmoji,
 | 
							"RenderEmoji":         RenderEmoji,
 | 
				
			||||||
		"ReactionToEmoji":     ReactionToEmoji,
 | 
							"ReactionToEmoji":     ReactionToEmoji,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,6 +130,17 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
 | 
				
			||||||
	return template.HTML(renderedText)
 | 
						return template.HTML(renderedText)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RenderRefIssueTitle renders referenced issue/pull title with defined post processors
 | 
				
			||||||
 | 
					func RenderRefIssueTitle(ctx context.Context, text string) template.HTML {
 | 
				
			||||||
 | 
						renderedText, err := markup.RenderRefIssueTitle(&markup.RenderContext{Ctx: ctx}, template.HTMLEscapeString(text))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("RenderRefIssueTitle: %v", err)
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return template.HTML(renderedText)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RenderLabel renders a label
 | 
					// RenderLabel renders a label
 | 
				
			||||||
// locale is needed due to an import cycle with our context providing the `Tr` function
 | 
					// locale is needed due to an import cycle with our context providing the `Tr` function
 | 
				
			||||||
func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
 | 
					func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ mail@domain.com
 | 
				
			||||||
@mention-user test
 | 
					@mention-user test
 | 
				
			||||||
#123
 | 
					#123
 | 
				
			||||||
  space
 | 
					  space
 | 
				
			||||||
`
 | 
					` + "`code :+1: #123 code`\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var testMetas = map[string]string{
 | 
					var testMetas = map[string]string{
 | 
				
			||||||
	"user":     "user13",
 | 
						"user":     "user13",
 | 
				
			||||||
| 
						 | 
					@ -115,8 +115,8 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 | 
				
			||||||
<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
 | 
					<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
 | 
				
			||||||
<a href="/mention-user" class="mention">@mention-user</a> test
 | 
					<a href="/mention-user" class="mention">@mention-user</a> test
 | 
				
			||||||
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
 | 
					<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
 | 
				
			||||||
  space`
 | 
					  space
 | 
				
			||||||
 | 
					` + "`code <span class=\"emoji\" aria-label=\"thumbs up\">👍</span> <a href=\"/user13/repo11/issues/123\" class=\"ref-issue\">#123</a> code`"
 | 
				
			||||||
	assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
 | 
						assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -153,10 +153,37 @@ mail@domain.com
 | 
				
			||||||
@mention-user test
 | 
					@mention-user test
 | 
				
			||||||
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
 | 
					<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
 | 
				
			||||||
  space
 | 
					  space
 | 
				
			||||||
 | 
					<code class="inline-code-block">code :+1: #123 code</code>
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
	assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
 | 
						assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRenderRefIssueTitle(t *testing.T) {
 | 
				
			||||||
 | 
						expected := `  space @mention-user  
 | 
				
			||||||
 | 
					/just/a/path.bin
 | 
				
			||||||
 | 
					https://example.com/file.bin
 | 
				
			||||||
 | 
					[local link](file.bin)
 | 
				
			||||||
 | 
					[remote link](https://example.com)
 | 
				
			||||||
 | 
					[[local link|file.bin]]
 | 
				
			||||||
 | 
					[[remote link|https://example.com]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[local image|image.jpg]]
 | 
				
			||||||
 | 
					[[remote link|https://example.com/image.jpg]]
 | 
				
			||||||
 | 
					https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
 | 
				
			||||||
 | 
					com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
 | 
				
			||||||
 | 
					https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
 | 
				
			||||||
 | 
					com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 | 
				
			||||||
 | 
					<span class="emoji" aria-label="thumbs up">👍</span>
 | 
				
			||||||
 | 
					mail@domain.com
 | 
				
			||||||
 | 
					@mention-user test
 | 
				
			||||||
 | 
					#123
 | 
				
			||||||
 | 
					  space
 | 
				
			||||||
 | 
					<code class="inline-code-block">code :+1: #123 code</code>
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
						assert.EqualValues(t, expected, RenderRefIssueTitle(context.Background(), testInput))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestRenderMarkdownToHtml(t *testing.T) {
 | 
					func TestRenderMarkdownToHtml(t *testing.T) {
 | 
				
			||||||
	expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
 | 
						expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
 | 
				
			||||||
/just/a/path.bin
 | 
					/just/a/path.bin
 | 
				
			||||||
| 
						 | 
					@ -177,7 +204,8 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 | 
				
			||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
 | 
					<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
 | 
				
			||||||
<a href="/mention-user" rel="nofollow">@mention-user</a> test
 | 
					<a href="/mention-user" rel="nofollow">@mention-user</a> test
 | 
				
			||||||
#123
 | 
					#123
 | 
				
			||||||
space</p>
 | 
					space
 | 
				
			||||||
 | 
					<code>code :+1: #123 code</code></p>
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
	assert.EqualValues(t, expected, RenderMarkdownToHtml(context.Background(), testInput))
 | 
						assert.EqualValues(t, expected, RenderMarkdownToHtml(context.Background(), testInput))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@
 | 
				
			||||||
			<div class="issue-card-icon">
 | 
								<div class="issue-card-icon">
 | 
				
			||||||
				{{template "shared/issueicon" .}}
 | 
									{{template "shared/issueicon" .}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{.Title | RenderEmoji ctx | RenderCodeBlock}}</a>
 | 
								<a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{RenderRefIssueTitle $.Context .Title}}</a>
 | 
				
			||||||
			{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
 | 
								{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
 | 
				
			||||||
				<a role="button" class="issue-card-unpin muted tw-flex tw-items-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
 | 
									<a role="button" class="issue-card-unpin muted tw-flex tw-items-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
 | 
				
			||||||
					{{svg "octicon-x" 16}}
 | 
										{{svg "octicon-x" 16}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -149,7 +149,7 @@
 | 
				
			||||||
				{{if eq .RefAction 3}}</del>{{end}}
 | 
									{{if eq .RefAction 3}}</del>{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<div class="detail flex-text-block">
 | 
									<div class="detail flex-text-block">
 | 
				
			||||||
					<span class="text grey muted-links"><a href="{{.RefIssueLink ctx}}"><b>{{.RefIssueTitle ctx}}</b> {{.RefIssueIdent ctx}}</a></span>
 | 
										<span class="text grey muted-links"><a href="{{.RefIssueLink ctx}}"><b>{{.RefIssueTitle ctx | RenderEmoji $.Context | RenderCodeBlock}}</b> {{.RefIssueIdent ctx}}</a></span>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		{{else if eq .Type 4}}
 | 
							{{else if eq .Type 4}}
 | 
				
			||||||
| 
						 | 
					@ -226,7 +226,7 @@
 | 
				
			||||||
				{{template "shared/user/avatarlink" dict "user" .Poster}}
 | 
									{{template "shared/user/avatarlink" dict "user" .Poster}}
 | 
				
			||||||
				<span class="text grey muted-links">
 | 
									<span class="text grey muted-links">
 | 
				
			||||||
					{{template "shared/user/authorlink" .Poster}}
 | 
										{{template "shared/user/authorlink" .Poster}}
 | 
				
			||||||
					{{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr}}
 | 
										{{ctx.Locale.Tr "repo.issues.change_title_at" (RenderRefIssueTitle $.Context .OldTitle) (RenderRefIssueTitle $.Context .NewTitle) $createdStr}}
 | 
				
			||||||
				</span>
 | 
									</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		{{else if eq .Type 11}}
 | 
							{{else if eq .Type 11}}
 | 
				
			||||||
| 
						 | 
					@ -339,10 +339,11 @@
 | 
				
			||||||
						{{svg "octicon-plus"}}
 | 
											{{svg "octicon-plus"}}
 | 
				
			||||||
						<span class="text grey muted-links">
 | 
											<span class="text grey muted-links">
 | 
				
			||||||
							<a href="{{.DependentIssue.Link}}">
 | 
												<a href="{{.DependentIssue.Link}}">
 | 
				
			||||||
 | 
													{{$strTitle := RenderRefIssueTitle $.Context .DependentIssue.Title}}
 | 
				
			||||||
								{{if eq .DependentIssue.RepoID .Issue.RepoID}}
 | 
													{{if eq .DependentIssue.RepoID .Issue.RepoID}}
 | 
				
			||||||
									#{{.DependentIssue.Index}} {{.DependentIssue.Title}}
 | 
														#{{.DependentIssue.Index}} {{$strTitle}}
 | 
				
			||||||
								{{else}}
 | 
													{{else}}
 | 
				
			||||||
									{{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{.DependentIssue.Title}}
 | 
														{{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{$strTitle}}
 | 
				
			||||||
								{{end}}
 | 
													{{end}}
 | 
				
			||||||
							</a>
 | 
												</a>
 | 
				
			||||||
						</span>
 | 
											</span>
 | 
				
			||||||
| 
						 | 
					@ -362,10 +363,11 @@
 | 
				
			||||||
						{{svg "octicon-trash"}}
 | 
											{{svg "octicon-trash"}}
 | 
				
			||||||
						<span class="text grey muted-links">
 | 
											<span class="text grey muted-links">
 | 
				
			||||||
							<a href="{{.DependentIssue.Link}}">
 | 
												<a href="{{.DependentIssue.Link}}">
 | 
				
			||||||
 | 
													{{$strTitle := RenderRefIssueTitle $.Context .DependentIssue.Title}}
 | 
				
			||||||
								{{if eq .DependentIssue.RepoID .Issue.RepoID}}
 | 
													{{if eq .DependentIssue.RepoID .Issue.RepoID}}
 | 
				
			||||||
									#{{.DependentIssue.Index}} {{.DependentIssue.Title}}
 | 
														#{{.DependentIssue.Index}} {{$strTitle}}
 | 
				
			||||||
								{{else}}
 | 
													{{else}}
 | 
				
			||||||
									{{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{.DependentIssue.Title}}
 | 
														{{.DependentIssue.Repo.FullName}}#{{.DependentIssue.Index}} - {{$strTitle}}
 | 
				
			||||||
								{{end}}
 | 
													{{end}}
 | 
				
			||||||
							</a>
 | 
												</a>
 | 
				
			||||||
						</span>
 | 
											</span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,8 +19,8 @@
 | 
				
			||||||
			{{range .BlockingDependencies}}
 | 
								{{range .BlockingDependencies}}
 | 
				
			||||||
				<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 | 
									<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 | 
				
			||||||
					<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 | 
										<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 | 
				
			||||||
						<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 | 
											<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{RenderRefIssueTitle $.Context .Issue.Title}}}">
 | 
				
			||||||
							#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
 | 
												#{{.Issue.Index}} {{RenderRefIssueTitle $.Context .Issue.Title}}}
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
						<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
 | 
											<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
 | 
				
			||||||
							{{.Repository.OwnerName}}/{{.Repository.Name}}
 | 
												{{.Repository.OwnerName}}/{{.Repository.Name}}
 | 
				
			||||||
| 
						 | 
					@ -51,8 +51,9 @@
 | 
				
			||||||
			{{range .BlockedByDependencies}}
 | 
								{{range .BlockedByDependencies}}
 | 
				
			||||||
				<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 | 
									<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 | 
				
			||||||
					<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 | 
										<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 | 
				
			||||||
						<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 | 
											{{$title := RenderRefIssueTitle $.Context .Issue.Title}}
 | 
				
			||||||
							#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
 | 
											<a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{RenderRefIssueTitle $.Context .Issue.Title}}">
 | 
				
			||||||
 | 
												#{{.Issue.Index}} {{RenderRefIssueTitle $.Context .Issue.Title}}
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
						<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
 | 
											<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
 | 
				
			||||||
							{{.Repository.OwnerName}}/{{.Repository.Name}}
 | 
												{{.Repository.OwnerName}}/{{.Repository.Name}}
 | 
				
			||||||
| 
						 | 
					@ -73,8 +74,8 @@
 | 
				
			||||||
						<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 | 
											<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 | 
				
			||||||
							<div class="gt-ellipsis">
 | 
												<div class="gt-ellipsis">
 | 
				
			||||||
								<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
 | 
													<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
 | 
				
			||||||
								<span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
 | 
													<span class="title" data-tooltip-content="#{{.Issue.Index}} {{RenderRefIssueTitle $.Context .DependentIssue.Title}}">
 | 
				
			||||||
									#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
 | 
														#{{.Issue.Index}} {{RenderRefIssueTitle $.Context .DependentIssue.Title}}
 | 
				
			||||||
								</span>
 | 
													</span>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
							<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
 | 
												<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,8 +7,7 @@
 | 
				
			||||||
	{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
 | 
						{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
 | 
				
			||||||
	<div class="issue-title" id="issue-title-display">
 | 
						<div class="issue-title" id="issue-title-display">
 | 
				
			||||||
		<h1 class="tw-break-anywhere">
 | 
							<h1 class="tw-break-anywhere">
 | 
				
			||||||
			{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
 | 
								{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx)}}				<span class="index">#{{.Issue.Index}}</span>
 | 
				
			||||||
			<span class="index">#{{.Issue.Index}}</span>
 | 
					 | 
				
			||||||
		</h1>
 | 
							</h1>
 | 
				
			||||||
		<div class="button-row">
 | 
							<div class="button-row">
 | 
				
			||||||
			{{if $canEditIssueTitle}}
 | 
								{{if $canEditIssueTitle}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -153,7 +153,7 @@
 | 
				
			||||||
		{{range .Activity.MergedPRs}}
 | 
							{{range .Activity.MergedPRs}}
 | 
				
			||||||
			<p class="desc">
 | 
								<p class="desc">
 | 
				
			||||||
				<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
 | 
									<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
 | 
				
			||||||
				#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
 | 
									#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{RenderRefIssueTitle $.Context .Issue.Title}}</a>
 | 
				
			||||||
				{{TimeSinceUnix .MergedUnix ctx.Locale}}
 | 
									{{TimeSinceUnix .MergedUnix ctx.Locale}}
 | 
				
			||||||
			</p>
 | 
								</p>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
| 
						 | 
					@ -172,7 +172,7 @@
 | 
				
			||||||
		{{range .Activity.OpenedPRs}}
 | 
							{{range .Activity.OpenedPRs}}
 | 
				
			||||||
			<p class="desc">
 | 
								<p class="desc">
 | 
				
			||||||
				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
 | 
									<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
 | 
				
			||||||
				#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
 | 
									#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{RenderRefIssueTitle $.Context .Issue.Title}}</a>
 | 
				
			||||||
				{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
 | 
									{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
 | 
				
			||||||
			</p>
 | 
								</p>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
| 
						 | 
					@ -191,7 +191,7 @@
 | 
				
			||||||
		{{range .Activity.ClosedIssues}}
 | 
							{{range .Activity.ClosedIssues}}
 | 
				
			||||||
			<p class="desc">
 | 
								<p class="desc">
 | 
				
			||||||
				<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
 | 
									<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
 | 
				
			||||||
				#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
 | 
									#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{RenderRefIssueTitle $.Context .Title}}</a>
 | 
				
			||||||
				{{TimeSinceUnix .ClosedUnix ctx.Locale}}
 | 
									{{TimeSinceUnix .ClosedUnix ctx.Locale}}
 | 
				
			||||||
			</p>
 | 
								</p>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
| 
						 | 
					@ -210,7 +210,7 @@
 | 
				
			||||||
		{{range .Activity.OpenedIssues}}
 | 
							{{range .Activity.OpenedIssues}}
 | 
				
			||||||
			<p class="desc">
 | 
								<p class="desc">
 | 
				
			||||||
				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
 | 
									<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
 | 
				
			||||||
				#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
 | 
									#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{RenderRefIssueTitle $.Context .Title}}</a>
 | 
				
			||||||
				{{TimeSinceUnix .CreatedUnix ctx.Locale}}
 | 
									{{TimeSinceUnix .CreatedUnix ctx.Locale}}
 | 
				
			||||||
			</p>
 | 
								</p>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
| 
						 | 
					@ -228,9 +228,9 @@
 | 
				
			||||||
				<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
 | 
									<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
 | 
				
			||||||
				#{{.Index}}
 | 
									#{{.Index}}
 | 
				
			||||||
				{{if .IsPull}}
 | 
									{{if .IsPull}}
 | 
				
			||||||
				<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
 | 
									<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{RenderRefIssueTitle $.Context .Title}}</a>
 | 
				
			||||||
				{{else}}
 | 
									{{else}}
 | 
				
			||||||
				<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
 | 
									<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{RenderRefIssueTitle $.Context .Title}}</a>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
				{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
 | 
									{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
 | 
				
			||||||
			</p>
 | 
								</p>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,7 +58,7 @@
 | 
				
			||||||
								<div class="notifications-bottom-row tw-text-16 tw-py-0.5">
 | 
													<div class="notifications-bottom-row tw-text-16 tw-py-0.5">
 | 
				
			||||||
									<span class="issue-title tw-break-anywhere">
 | 
														<span class="issue-title tw-break-anywhere">
 | 
				
			||||||
										{{if .Issue}}
 | 
															{{if .Issue}}
 | 
				
			||||||
											{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}
 | 
																{{RenderRefIssueTitle $.Context .Issue.Title}}
 | 
				
			||||||
										{{else}}
 | 
															{{else}}
 | 
				
			||||||
											{{.Repository.FullName}}
 | 
																{{.Repository.FullName}}
 | 
				
			||||||
										{{end}}
 | 
															{{end}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										162
									
								
								tests/integration/repo_issue_title_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								tests/integration/repo_issue_title_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,162 @@
 | 
				
			||||||
 | 
					// Copyright 2024 The Forgejo Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
 | 
						issue_service "code.gitea.io/gitea/services/issue"
 | 
				
			||||||
 | 
						pull_service "code.gitea.io/gitea/services/pull"
 | 
				
			||||||
 | 
						files_service "code.gitea.io/gitea/services/repository/files"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIssueTitles(t *testing.T) {
 | 
				
			||||||
 | 
						onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
				
			||||||
 | 
							user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
				
			||||||
 | 
							repo, _, f := tests.CreateDeclarativeRepo(t, user, "issue-titles", nil, nil, nil)
 | 
				
			||||||
 | 
							defer f()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							session := loginUser(t, user.LoginName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							title := "Title :+1: `code`"
 | 
				
			||||||
 | 
							issue1 := createIssue(t, user, repo, title, "Test issue")
 | 
				
			||||||
 | 
							issue2 := createIssue(t, user, repo, title, "Ref #1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							titleHTML := []string{
 | 
				
			||||||
 | 
								"Title",
 | 
				
			||||||
 | 
								`<span class="emoji" aria-label="thumbs up">👍</span>`,
 | 
				
			||||||
 | 
								`<code class="inline-code-block">code</code>`,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Main issue title", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								html := extractHTML(t, session, issue1, "div.issue-title-header > * > h1")
 | 
				
			||||||
 | 
								assertContainsAll(t, titleHTML, html)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Referenced issue comment", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								html := extractHTML(t, session, issue1, "div.timeline > div.timeline-item:nth-child(3) > div.detail > * > a")
 | 
				
			||||||
 | 
								assertContainsAll(t, titleHTML, html)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Dependent issue comment", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err := issues_model.CreateIssueDependency(db.DefaultContext, user, issue1, issue2)
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								html := extractHTML(t, session, issue1, "div.timeline > div:nth-child(3) > div.detail > * > a")
 | 
				
			||||||
 | 
								assertContainsAll(t, titleHTML, html)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Dependent issue sidebar", func(t *testing.T) {
 | 
				
			||||||
 | 
								defer tests.PrintCurrentTest(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								html := extractHTML(t, session, issue1, "div.item.dependency > * > a.title")
 | 
				
			||||||
 | 
								assertContainsAll(t, titleHTML, html)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Run("Referenced pull comment", func(t *testing.T) {
 | 
				
			||||||
 | 
								_, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
 | 
				
			||||||
 | 
									Files: []*files_service.ChangeRepoFile{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											Operation:     "update",
 | 
				
			||||||
 | 
											TreePath:      "README.md",
 | 
				
			||||||
 | 
											ContentReader: strings.NewReader("Update README"),
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Message:   "Update README",
 | 
				
			||||||
 | 
									OldBranch: "main",
 | 
				
			||||||
 | 
									NewBranch: "branch",
 | 
				
			||||||
 | 
									Author: &files_service.IdentityOptions{
 | 
				
			||||||
 | 
										Name:  user.Name,
 | 
				
			||||||
 | 
										Email: user.Email,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Committer: &files_service.IdentityOptions{
 | 
				
			||||||
 | 
										Name:  user.Name,
 | 
				
			||||||
 | 
										Email: user.Email,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Dates: &files_service.CommitDateOptions{
 | 
				
			||||||
 | 
										Author:    time.Now(),
 | 
				
			||||||
 | 
										Committer: time.Now(),
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								pullIssue := &issues_model.Issue{
 | 
				
			||||||
 | 
									RepoID:   repo.ID,
 | 
				
			||||||
 | 
									Title:    title,
 | 
				
			||||||
 | 
									Content:  "Closes #1",
 | 
				
			||||||
 | 
									PosterID: user.ID,
 | 
				
			||||||
 | 
									Poster:   user,
 | 
				
			||||||
 | 
									IsPull:   true,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								pullRequest := &issues_model.PullRequest{
 | 
				
			||||||
 | 
									HeadRepoID: repo.ID,
 | 
				
			||||||
 | 
									BaseRepoID: repo.ID,
 | 
				
			||||||
 | 
									HeadBranch: "branch",
 | 
				
			||||||
 | 
									BaseBranch: "main",
 | 
				
			||||||
 | 
									HeadRepo:   repo,
 | 
				
			||||||
 | 
									BaseRepo:   repo,
 | 
				
			||||||
 | 
									Type:       issues_model.PullRequestGitea,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err = pull_service.NewPullRequest(git.DefaultContext, repo, pullIssue, nil, nil, pullRequest, nil)
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								html := extractHTML(t, session, issue1, "div.timeline > div:nth-child(4) > div.detail > * > a")
 | 
				
			||||||
 | 
								assertContainsAll(t, titleHTML, html)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createIssue(t *testing.T, user *user_model.User, repo *repo_model.Repository, title, content string) *issues_model.Issue {
 | 
				
			||||||
 | 
						issue := &issues_model.Issue{
 | 
				
			||||||
 | 
							RepoID:   repo.ID,
 | 
				
			||||||
 | 
							Title:    title,
 | 
				
			||||||
 | 
							Content:  content,
 | 
				
			||||||
 | 
							PosterID: user.ID,
 | 
				
			||||||
 | 
							Poster:   user,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := issue_service.NewIssue(db.DefaultContext, repo, issue, nil, nil, nil)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return issue
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func extractHTML(t *testing.T, session *TestSession, issue *issues_model.Issue, query string) string {
 | 
				
			||||||
 | 
						req := NewRequest(t, "GET", issue.HTMLURL())
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						doc := NewHTMLParser(t, resp.Body)
 | 
				
			||||||
 | 
						res, err := doc.doc.Find(query).Html()
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return res
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func assertContainsAll(t *testing.T, expected []string, actual string) {
 | 
				
			||||||
 | 
						for i := range expected {
 | 
				
			||||||
 | 
							assert.Contains(t, actual, expected[i])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import {defineConfig} from 'vitest/config';
 | 
					import {defineConfig} from 'vitest/config';
 | 
				
			||||||
import vuePlugin from '@vitejs/plugin-vue';
 | 
					import vuePlugin from '@vitejs/plugin-vue';
 | 
				
			||||||
import {stringPlugin} from 'vite-string-plugin';
 | 
					import {stringPlugin} from 'vite-string-plugin';
 | 
				
			||||||
 | 
					import {resolve} from 'node:path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  test: {
 | 
					  test: {
 | 
				
			||||||
| 
						 | 
					@ -13,6 +14,9 @@ export default defineConfig({
 | 
				
			||||||
    passWithNoTests: true,
 | 
					    passWithNoTests: true,
 | 
				
			||||||
    globals: true,
 | 
					    globals: true,
 | 
				
			||||||
    watch: false,
 | 
					    watch: false,
 | 
				
			||||||
 | 
					    alias: {
 | 
				
			||||||
 | 
					      'monaco-editor': resolve(import.meta.dirname, '/node_modules/monaco-editor/esm/vs/editor/editor.api'),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  plugins: [
 | 
					  plugins: [
 | 
				
			||||||
    stringPlugin(),
 | 
					    stringPlugin(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ import {toAbsoluteUrl} from '../utils.js';
 | 
				
			||||||
import {initDropzone} from './common-global.js';
 | 
					import {initDropzone} from './common-global.js';
 | 
				
			||||||
import {POST, GET} from '../modules/fetch.js';
 | 
					import {POST, GET} from '../modules/fetch.js';
 | 
				
			||||||
import {showErrorToast} from '../modules/toast.js';
 | 
					import {showErrorToast} from '../modules/toast.js';
 | 
				
			||||||
 | 
					import {emojiHTML} from './emoji.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {appSubUrl} = window.config;
 | 
					const {appSubUrl} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -124,7 +125,7 @@ export function initRepoIssueSidebarList() {
 | 
				
			||||||
              return;
 | 
					              return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            filteredResponse.results.push({
 | 
					            filteredResponse.results.push({
 | 
				
			||||||
              name: `#${issue.number} ${htmlEscape(issue.title)
 | 
					              name: `#${issue.number} ${issueTitleHTML(htmlEscape(issue.title))
 | 
				
			||||||
              }<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
 | 
					              }<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
 | 
				
			||||||
              value: issue.id,
 | 
					              value: issue.id,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
| 
						 | 
					@ -731,3 +732,9 @@ export function initArchivedLabelHandler() {
 | 
				
			||||||
    toggleElem(label, label.classList.contains('checked'));
 | 
					    toggleElem(label, label.classList.contains('checked'));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Render the issue's title. It converts emojis and code blocks syntax into their respective HTML equivalent.
 | 
				
			||||||
 | 
					export function issueTitleHTML(title) {
 | 
				
			||||||
 | 
					  return title.replaceAll(/:[-+\w]+:/g, (emoji) => emojiHTML(emoji.substring(1, emoji.length - 1)))
 | 
				
			||||||
 | 
					    .replaceAll(/`[^`]+`/g, (code) => `<code class="inline-code-block">${code.substring(1, code.length - 1)}</code>`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										24
									
								
								web_src/js/features/repo-issue.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								web_src/js/features/repo-issue.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					import {vi} from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {issueTitleHTML} from './repo-issue.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// monaco-editor does not have any exports fields, which trips up vitest
 | 
				
			||||||
 | 
					vi.mock('./comp/ComboMarkdownEditor.js', () => ({}));
 | 
				
			||||||
 | 
					// jQuery is missing
 | 
				
			||||||
 | 
					vi.mock('./common-global.js', () => ({}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Convert issue title to html', () => {
 | 
				
			||||||
 | 
					  expect(issueTitleHTML('')).toEqual('');
 | 
				
			||||||
 | 
					  expect(issueTitleHTML('issue title')).toEqual('issue title');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const expected_thumbs_up = `<span class="emoji" title=":+1:">👍</span>`;
 | 
				
			||||||
 | 
					  expect(issueTitleHTML(':+1:')).toEqual(expected_thumbs_up);
 | 
				
			||||||
 | 
					  expect(issueTitleHTML(':invalid emoji:')).toEqual(':invalid emoji:');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const expected_code_block = `<code class="inline-code-block">code</code>`;
 | 
				
			||||||
 | 
					  expect(issueTitleHTML('`code`')).toEqual(expected_code_block);
 | 
				
			||||||
 | 
					  expect(issueTitleHTML('`invalid code')).toEqual('`invalid code');
 | 
				
			||||||
 | 
					  expect(issueTitleHTML('invalid code`')).toEqual('invalid code`');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  expect(issueTitleHTML('issue title :+1: `code`')).toEqual(`issue title ${expected_thumbs_up} ${expected_code_block}`);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue