Feature: Find files in repo (#15028)
* Create finding files page ui in repo page * Get tree entries for find repo files. * Move find files JS to individual file. * gen swagger. * Add enry.IsVendor to exclude entries Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		
					parent
					
						
							
								7948cb3149
							
						
					
				
			
			
				commit
				
					
						2ae45cebbf
					
				
			
		
					 13 changed files with 235 additions and 3 deletions
				
			
		| 
						 | 
					@ -9,6 +9,7 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"xorm.io/builder"
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,12 +8,12 @@ import (
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_ "code.gitea.io/gitea/models"             // register table model
 | 
						_ "code.gitea.io/gitea/models"             // register table model
 | 
				
			||||||
	_ "code.gitea.io/gitea/models/perm/access" // register table model
 | 
						_ "code.gitea.io/gitea/models/perm/access" // register table model
 | 
				
			||||||
	_ "code.gitea.io/gitea/models/repo"        // register table model
 | 
						_ "code.gitea.io/gitea/models/repo"        // register table model
 | 
				
			||||||
	_ "code.gitea.io/gitea/models/user"        // register table model
 | 
						_ "code.gitea.io/gitea/models/user"        // register table model
 | 
				
			||||||
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestMain(m *testing.M) {
 | 
					func TestMain(m *testing.M) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2286,6 +2286,9 @@ topic.done = Done
 | 
				
			||||||
topic.count_prompt = You can not select more than 25 topics
 | 
					topic.count_prompt = You can not select more than 25 topics
 | 
				
			||||||
topic.format_prompt = Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 | 
					topic.format_prompt = Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					find_file.go_to_file = Go to file
 | 
				
			||||||
 | 
					find_file.no_matching = No matching file found
 | 
				
			||||||
 | 
					
 | 
				
			||||||
error.csv.too_large = Can't render this file because it is too large.
 | 
					error.csv.too_large = Can't render this file because it is too large.
 | 
				
			||||||
error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d.
 | 
					error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d.
 | 
				
			||||||
error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
 | 
					error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										24
									
								
								routers/web/repo/find.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								routers/web/repo/find.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						tplFindFiles base.TplName = "repo/find/files"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FindFiles render the page to find repository files
 | 
				
			||||||
 | 
					func FindFiles(ctx *context.Context) {
 | 
				
			||||||
 | 
						path := ctx.Params("*")
 | 
				
			||||||
 | 
						ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + path
 | 
				
			||||||
 | 
						ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + path
 | 
				
			||||||
 | 
						ctx.HTML(http.StatusOK, tplFindFiles)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										55
									
								
								routers/web/repo/treelist.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								routers/web/repo/treelist.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/go-enry/go-enry/v2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TreeList get all files' entries of a repository
 | 
				
			||||||
 | 
					func TreeList(ctx *context.Context) {
 | 
				
			||||||
 | 
						tree, err := ctx.Repo.Commit.SubTree("/")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("Repo.Commit.SubTree", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						entries, err := tree.ListEntriesRecursive()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("ListEntriesRecursive", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						entries.CustomSort(base.NaturalSortLess)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						files := make([]string, 0, len(entries))
 | 
				
			||||||
 | 
						for _, entry := range entries {
 | 
				
			||||||
 | 
							if !isExcludedEntry(entry) {
 | 
				
			||||||
 | 
								files = append(files, entry.Name())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, files)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isExcludedEntry(entry *git.TreeEntry) bool {
 | 
				
			||||||
 | 
						if entry.IsDir() {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if entry.IsSubModule() {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if enry.IsVendor(entry.Name()) {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -831,6 +831,12 @@ func RegisterRoutes(m *web.Route) {
 | 
				
			||||||
		m.Group("/milestone", func() {
 | 
							m.Group("/milestone", func() {
 | 
				
			||||||
			m.Get("/{id}", repo.MilestoneIssuesAndPulls)
 | 
								m.Get("/{id}", repo.MilestoneIssuesAndPulls)
 | 
				
			||||||
		}, reqRepoIssuesOrPullsReader, context.RepoRef())
 | 
							}, reqRepoIssuesOrPullsReader, context.RepoRef())
 | 
				
			||||||
 | 
							m.Get("/find/*", repo.FindFiles)
 | 
				
			||||||
 | 
							m.Group("/tree-list", func() {
 | 
				
			||||||
 | 
								m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.TreeList)
 | 
				
			||||||
 | 
								m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.TreeList)
 | 
				
			||||||
 | 
								m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.TreeList)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
		m.Get("/compare", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists, ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
 | 
							m.Get("/compare", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists, ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
 | 
				
			||||||
		m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
 | 
							m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
 | 
				
			||||||
			Get(ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
 | 
								Get(ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								templates/repo/find/files.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								templates/repo/find/files.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					{{template "base/head" .}}
 | 
				
			||||||
 | 
					<div class="page-content repository">
 | 
				
			||||||
 | 
						{{template "repo/header" .}}
 | 
				
			||||||
 | 
						<div class="ui container">
 | 
				
			||||||
 | 
							<div class="df ac">
 | 
				
			||||||
 | 
								<a href="{{$.RepoLink}}">{{.RepoName}}</a>
 | 
				
			||||||
 | 
								<span class="mx-3">/</span>
 | 
				
			||||||
 | 
								<div class="ui input f1">
 | 
				
			||||||
 | 
									<input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<table id="repo-find-file-table" class="ui single line table">
 | 
				
			||||||
 | 
								<tbody>
 | 
				
			||||||
 | 
								</tbody>
 | 
				
			||||||
 | 
							</table>
 | 
				
			||||||
 | 
							<div id="repo-find-file-no-result" class="ui row center mt-5" hidden>
 | 
				
			||||||
 | 
								<h3>{{.i18n.Tr "repo.find_file.no_matching"}}</h3>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{template "base/footer" .}}
 | 
				
			||||||
| 
						 | 
					@ -73,6 +73,11 @@
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
 | 
									<div class="fitted item mx-0">
 | 
				
			||||||
 | 
										<a href="{{.BaseRepo.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">
 | 
				
			||||||
 | 
											{{.i18n.Tr "repo.find_file.go_to_file"}}
 | 
				
			||||||
 | 
										</a>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
			{{else}}
 | 
								{{else}}
 | 
				
			||||||
				<div class="fitted item"><span class="ui breadcrumb repo-path"><a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{EllipsisString .Repository.Name 30}}</a>{{range $i, $v := .TreeNames}}<span class="divider">/</span>{{if eq $i $l}}<span class="active section" title="{{$v}}">{{EllipsisString $v 30}}</span>{{else}}{{ $p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{EllipsisString $v 30}}</a></span>{{end}}{{end}}</span></div>
 | 
									<div class="fitted item"><span class="ui breadcrumb repo-path"><a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{EllipsisString .Repository.Name 30}}</a>{{range $i, $v := .TreeNames}}<span class="divider">/</span>{{if eq $i $l}}<span class="active section" title="{{$v}}">{{EllipsisString $v 30}}</span>{{else}}{{ $p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{EllipsisString $v 30}}</a></span>{{end}}{{end}}</span></div>
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										67
									
								
								web_src/js/features/repo-findfile.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								web_src/js/features/repo-findfile.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,67 @@
 | 
				
			||||||
 | 
					import $ from 'jquery';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {svg} from '../svg.js';
 | 
				
			||||||
 | 
					import {strSubMatch} from '../utils.js';
 | 
				
			||||||
 | 
					const {csrf} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const threshold = 50;
 | 
				
			||||||
 | 
					let files = [];
 | 
				
			||||||
 | 
					let $repoFindFileInput, $repoFindFileTableBody, $repoFindFileNoResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function filterRepoFiles(filter) {
 | 
				
			||||||
 | 
					  const treeLink = $repoFindFileInput.attr('data-url-tree-link');
 | 
				
			||||||
 | 
					  $repoFindFileTableBody.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fileRes = [];
 | 
				
			||||||
 | 
					  if (filter) {
 | 
				
			||||||
 | 
					    for (let i = 0; i < files.length && fileRes.length < threshold; i++) {
 | 
				
			||||||
 | 
					      const subMatch = strSubMatch(files[i], filter);
 | 
				
			||||||
 | 
					      if (subMatch.length > 1) {
 | 
				
			||||||
 | 
					        fileRes.push(subMatch);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    for (let i = 0; i < files.length && i < threshold; i++) {
 | 
				
			||||||
 | 
					      fileRes.push([files[i]]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const tmplRow = `<tr><td><a></a></td></tr>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $repoFindFileNoResult.toggle(fileRes.length === 0);
 | 
				
			||||||
 | 
					  for (const matchRes of fileRes) {
 | 
				
			||||||
 | 
					    const $row = $(tmplRow);
 | 
				
			||||||
 | 
					    const $a = $row.find('a');
 | 
				
			||||||
 | 
					    $a.attr('href', `${treeLink}/${matchRes.join('')}`);
 | 
				
			||||||
 | 
					    const $octiconFile = $(svg('octicon-file')).addClass('mr-3');
 | 
				
			||||||
 | 
					    $a.append($octiconFile);
 | 
				
			||||||
 | 
					    // if the target file path is "abc/xyz", to search "bx", then the matchRes is ['a', 'b', 'c/', 'x', 'yz']
 | 
				
			||||||
 | 
					    // the matchRes[odd] is matched and highlighted to red.
 | 
				
			||||||
 | 
					    for (let j = 0; j < matchRes.length; j++) {
 | 
				
			||||||
 | 
					      if (!matchRes[j]) continue;
 | 
				
			||||||
 | 
					      const $span = $('<span>').text(matchRes[j]);
 | 
				
			||||||
 | 
					      if (j % 2 === 1) $span.addClass('ui text red');
 | 
				
			||||||
 | 
					      $a.append($span);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    $repoFindFileTableBody.append($row);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function loadRepoFiles() {
 | 
				
			||||||
 | 
					  files = await $.ajax({
 | 
				
			||||||
 | 
					    url: $repoFindFileInput.attr('data-url-data-link'),
 | 
				
			||||||
 | 
					    headers: {'X-Csrf-Token': csrf}
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  filterRepoFiles($repoFindFileInput.val());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function initFindFileInRepo() {
 | 
				
			||||||
 | 
					  $repoFindFileInput = $('#repo-file-find-input');
 | 
				
			||||||
 | 
					  if (!$repoFindFileInput.length) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $repoFindFileTableBody = $('#repo-find-file-table tbody');
 | 
				
			||||||
 | 
					  $repoFindFileNoResult = $('#repo-find-file-no-result');
 | 
				
			||||||
 | 
					  $repoFindFileInput.on('input', () => filterRepoFiles($repoFindFileInput.val()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loadRepoFiles();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ import {initMarkupAnchors} from './markup/anchors.js';
 | 
				
			||||||
import {initNotificationCount, initNotificationsTable} from './features/notification.js';
 | 
					import {initNotificationCount, initNotificationsTable} from './features/notification.js';
 | 
				
			||||||
import {initRepoIssueContentHistory} from './features/repo-issue-content.js';
 | 
					import {initRepoIssueContentHistory} from './features/repo-issue-content.js';
 | 
				
			||||||
import {initStopwatch} from './features/stopwatch.js';
 | 
					import {initStopwatch} from './features/stopwatch.js';
 | 
				
			||||||
 | 
					import {initFindFileInRepo} from './features/repo-findfile.js';
 | 
				
			||||||
import {initCommentContent, initMarkupContent} from './markup/content.js';
 | 
					import {initCommentContent, initMarkupContent} from './markup/content.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js';
 | 
					import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js';
 | 
				
			||||||
| 
						 | 
					@ -124,6 +125,7 @@ $(document).ready(() => {
 | 
				
			||||||
  initSshKeyFormParser();
 | 
					  initSshKeyFormParser();
 | 
				
			||||||
  initStopwatch();
 | 
					  initStopwatch();
 | 
				
			||||||
  initTableSort();
 | 
					  initTableSort();
 | 
				
			||||||
 | 
					  initFindFileInRepo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  initAdminCommon();
 | 
					  initAdminCommon();
 | 
				
			||||||
  initAdminEmails();
 | 
					  initAdminEmails();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@ import octiconRepo from '../../public/img/svg/octicon-repo.svg';
 | 
				
			||||||
import octiconRepoForked from '../../public/img/svg/octicon-repo-forked.svg';
 | 
					import octiconRepoForked from '../../public/img/svg/octicon-repo-forked.svg';
 | 
				
			||||||
import octiconRepoTemplate from '../../public/img/svg/octicon-repo-template.svg';
 | 
					import octiconRepoTemplate from '../../public/img/svg/octicon-repo-template.svg';
 | 
				
			||||||
import octiconTriangleDown from '../../public/img/svg/octicon-triangle-down.svg';
 | 
					import octiconTriangleDown from '../../public/img/svg/octicon-triangle-down.svg';
 | 
				
			||||||
 | 
					import octiconFile from '../../public/img/svg/octicon-file.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +37,7 @@ export const svgs = {
 | 
				
			||||||
  'octicon-repo-forked': octiconRepoForked,
 | 
					  'octicon-repo-forked': octiconRepoForked,
 | 
				
			||||||
  'octicon-repo-template': octiconRepoTemplate,
 | 
					  'octicon-repo-template': octiconRepoTemplate,
 | 
				
			||||||
  'octicon-triangle-down': octiconTriangleDown,
 | 
					  'octicon-triangle-down': octiconTriangleDown,
 | 
				
			||||||
 | 
					  'octicon-file': octiconFile,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const parser = new DOMParser();
 | 
					const parser = new DOMParser();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,3 +58,35 @@ export function parseIssueHref(href) {
 | 
				
			||||||
  const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
 | 
					  const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
 | 
				
			||||||
  return {owner, repo, type, index};
 | 
					  return {owner, repo, type, index};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// return the sub-match result as an array:  [unmatched, matched, unmatched, matched, ...]
 | 
				
			||||||
 | 
					// res[even] is unmatched, res[odd] is matched, see unit tests for examples
 | 
				
			||||||
 | 
					export function strSubMatch(full, sub) {
 | 
				
			||||||
 | 
					  const res = [''];
 | 
				
			||||||
 | 
					  let i = 0, j = 0;
 | 
				
			||||||
 | 
					  for (; i < sub.length && j < full.length;) {
 | 
				
			||||||
 | 
					    while (j < full.length) {
 | 
				
			||||||
 | 
					      if (sub[i] === full[j]) {
 | 
				
			||||||
 | 
					        if (res.length % 2 !== 0) res.push('');
 | 
				
			||||||
 | 
					        res[res.length - 1] += full[j];
 | 
				
			||||||
 | 
					        j++;
 | 
				
			||||||
 | 
					        i++;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        if (res.length % 2 === 0) res.push('');
 | 
				
			||||||
 | 
					        res[res.length - 1] += full[j];
 | 
				
			||||||
 | 
					        j++;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (i !== sub.length) {
 | 
				
			||||||
 | 
					    // if the sub string doesn't match the full, only return the full as unmatched.
 | 
				
			||||||
 | 
					    return [full];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (j < full.length) {
 | 
				
			||||||
 | 
					    // append remaining chars from full to result as unmatched
 | 
				
			||||||
 | 
					    if (res.length % 2 === 0) res.push('');
 | 
				
			||||||
 | 
					    res[res.length - 1] += full.substring(j);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return res;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref,
 | 
					  basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, strSubMatch,
 | 
				
			||||||
} from './utils.js';
 | 
					} from './utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('basename', () => {
 | 
					test('basename', () => {
 | 
				
			||||||
| 
						 | 
					@ -84,3 +84,17 @@ test('parseIssueHref', () => {
 | 
				
			||||||
  expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
 | 
					  expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
 | 
				
			||||||
  expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined});
 | 
					  expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('strSubMatch', () => {
 | 
				
			||||||
 | 
					  expect(strSubMatch('abc', '')).toEqual(['abc']);
 | 
				
			||||||
 | 
					  expect(strSubMatch('abc', 'a')).toEqual(['', 'a', 'bc']);
 | 
				
			||||||
 | 
					  expect(strSubMatch('abc', 'b')).toEqual(['a', 'b', 'c']);
 | 
				
			||||||
 | 
					  expect(strSubMatch('abc', 'c')).toEqual(['ab', 'c']);
 | 
				
			||||||
 | 
					  expect(strSubMatch('abc', 'ac')).toEqual(['', 'a', 'b', 'c']);
 | 
				
			||||||
 | 
					  expect(strSubMatch('abc', 'z')).toEqual(['abc']);
 | 
				
			||||||
 | 
					  expect(strSubMatch('abc', 'az')).toEqual(['abc']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  expect(strSubMatch('aabbcc', 'abc')).toEqual(['', 'a', 'a', 'b', 'b', 'c', 'c']);
 | 
				
			||||||
 | 
					  expect(strSubMatch('the/directory', 'hedir')).toEqual(['t', 'he', '/', 'dir', 'ectory']);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue