Merge remote-tracking branch 'upstream/main'
This commit is contained in:
		
						commit
						47229ea208
					
				
					 41 changed files with 1206 additions and 288 deletions
				
			
		|  | @ -1262,6 +1262,9 @@ ROUTER = console | ||||||
| ;; List of file extensions that should be rendered/edited as Markdown | ;; List of file extensions that should be rendered/edited as Markdown | ||||||
| ;; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma | ;; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma | ||||||
| ;FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd | ;FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd | ||||||
|  | ;; | ||||||
|  | ;; Enables math inline and block detection | ||||||
|  | ;ENABLE_MATH = true | ||||||
| 
 | 
 | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|  |  | ||||||
|  | @ -236,6 +236,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a | ||||||
| - `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional | - `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional | ||||||
|   URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are |   URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are | ||||||
|   always displayed |   always displayed | ||||||
|  | - `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]`, `$...$` and `$$...$$` blocks as math blocks. | ||||||
| 
 | 
 | ||||||
| ## Server (`server`) | ## Server (`server`) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -74,12 +74,13 @@ RENDER_COMMAND = "timeout 30s pandoc +RTS -M512M -RTS -f rst" | ||||||
| IS_INPUT_FILE = false | IS_INPUT_FILE = false | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| If your external markup relies on additional classes and attributes on the generated HTML elements, you might need to enable custom sanitizer policies. Gitea uses the [`bluemonday`](https://godoc.org/github.com/microcosm-cc/bluemonday) package as our HTML sanitizier. The example below will support [KaTeX](https://katex.org/) output from [`pandoc`](https://pandoc.org/). | If your external markup relies on additional classes and attributes on the generated HTML elements, you might need to enable custom sanitizer policies. Gitea uses the [`bluemonday`](https://godoc.org/github.com/microcosm-cc/bluemonday) package as our HTML sanitizer. The example below could be used to support server-side [KaTeX](https://katex.org/) rendering output from [`pandoc`](https://pandoc.org/). | ||||||
| 
 | 
 | ||||||
| ```ini | ```ini | ||||||
| [markup.sanitizer.TeX] | [markup.sanitizer.TeX] | ||||||
| ; Pandoc renders TeX segments as <span>s with the "math" class, optionally | ; Pandoc renders TeX segments as <span>s with the "math" class, optionally | ||||||
| ; with "inline" or "display" classes depending on context. | ; with "inline" or "display" classes depending on context. | ||||||
|  | ; - note this is different from the built-in math support in our markdown parser which uses <code> | ||||||
| ELEMENT = span | ELEMENT = span | ||||||
| ALLOW_ATTR = class | ALLOW_ATTR = class | ||||||
| REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+ | REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+ | ||||||
|  |  | ||||||
|  | @ -53,6 +53,8 @@ _Symbols used in table:_ | ||||||
| | WebAuthn (2FA)                      | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✓              | ?            | | | WebAuthn (2FA)                      | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✓              | ?            | | ||||||
| | Built-in CI/CD                      | ✘                                                  | ✘    | ✓         | ✓         | ✓         | ✘              | ✘            | | | Built-in CI/CD                      | ✘                                                  | ✘    | ✓         | ✓         | ✓         | ✘              | ✘            | | ||||||
| | Subgroups: groups within groups     | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘    | ✘         | ✓         | ✓         | ✘              | ✓            | | | Subgroups: groups within groups     | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘    | ✘         | ✓         | ✓         | ✘              | ✓            | | ||||||
|  | | Mermaid diagrams in Markdown        | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✘              | ✘            | | ||||||
|  | | Math syntax in Markdown             | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✘              | ✘            | | ||||||
| 
 | 
 | ||||||
| ## Code management | ## Code management | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ _表格中的符号含义:_ | ||||||
| #### 主要特性 | #### 主要特性 | ||||||
| 
 | 
 | ||||||
| | 特性                  | Gitea                                              | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket      | RhodeCode CE | | | 特性                  | Gitea                                              | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket      | RhodeCode CE | | ||||||
| |-----------------------|-------|------|-----------|-----------|-----------|-----------|--------------| | | --------------------- | -------------------------------------------------- | ---- | --------- | --------- | --------- | -------------- | ------------ | | ||||||
| | 开源免费              | ✓                                                  | ✓    | ✘         | ✓         | ✘         | ✘              | ✓            | | | 开源免费              | ✓                                                  | ✓    | ✘         | ✓         | ✘         | ✘              | ✓            | | ||||||
| | 低资源开销 (RAM/CPU)  | ✓                                                  | ✓    | ✘         | ✘         | ✘         | ✘              | ✘            | | | 低资源开销 (RAM/CPU)  | ✓                                                  | ✓    | ✘         | ✘         | ✘         | ✘              | ✘            | | ||||||
| | 支持多种数据库        | ✓                                                  | ✓    | ✘         | ⁄         | ⁄         | ✓              | ✓            | | | 支持多种数据库        | ✓                                                  | ✓    | ✘         | ⁄         | ⁄         | ✓              | ✓            | | ||||||
|  | @ -42,79 +42,79 @@ _表格中的符号含义:_ | ||||||
| | 支持 Orgmode          | ✓                                                  | ✘    | ✓         | ✘         | ✘         | ✘              | ?            | | | 支持 Orgmode          | ✓                                                  | ✘    | ✓         | ✘         | ✘         | ✘              | ?            | | ||||||
| | 支持 CSV              | ✓                                                  | ✘    | ✓         | ✘         | ✘         | ✓              | ?            | | | 支持 CSV              | ✓                                                  | ✘    | ✓         | ✘         | ✘         | ✓              | ?            | | ||||||
| | 支持第三方渲染工具    | ✓                                                  | ✘    | ✘         | ✘         | ✘         | ✓              | ?            | | | 支持第三方渲染工具    | ✓                                                  | ✘    | ✘         | ✘         | ✘         | ✓              | ?            | | ||||||
| | Git 驱动的静态 pages  | ✘     | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | | | Git 驱动的静态 pages  | [✘](https://github.com/go-gitea/gitea/issues/302)  | ✘    | ✓         | ✓         | ✓         | ✘              | ✘            | | ||||||
| | Git 驱动的集成化 wiki | ✓     | ✓    | ✓         | ✓         | ✓         | ✓         | ✘            | | | Git 驱动的集成化 wiki | ✓                                                  | ✓    | ✓         | ✓         | ✓         | ✓ (cloud only) | ✘            | | ||||||
| | 部署令牌              | ✓                                                  | ✓    | ✓         | ✓         | ✓         | ✓              | ✓            | | | 部署令牌              | ✓                                                  | ✓    | ✓         | ✓         | ✓         | ✓              | ✓            | | ||||||
| | 仓库写权限令牌        | ✓     | ✘    | ✓         | ✓         | ✓         | ✘         | ✓            | | | 仓库写权限令牌        | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✓              | ✓            | | ||||||
| | 内置容器 Registry     | ✓     | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 内置容器 Registry     | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✘              | ✘            | | ||||||
| | 外部 Git 镜像         | ✓                                                  | ✓    | ✘         | ✘         | ✓         | ✓              | ✓            | | | 外部 Git 镜像         | ✓                                                  | ✓    | ✘         | ✘         | ✓         | ✓              | ✓            | | ||||||
| | WebAuthn (2FA)        | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✓              | ?            | | | WebAuthn (2FA)        | ✓                                                  | ✘    | ✓         | ✓         | ✓         | ✓              | ?            | | ||||||
| | 内置 CI/CD            | ✘     | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 内置 CI/CD            | ✘                                                  | ✘    | ✓         | ✓         | ✓         | ✘              | ✘            | | ||||||
| | 子组织:组织内的组织  | ✘     | ✘    | ✘         | ✓         | ✓         | ✘         | ✓            | | | 子组织:组织内的组织  | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘    | ✘         | ✓         | ✓         | ✘              | ✓            | | ||||||
| 
 | 
 | ||||||
| #### 代码管理 | #### 代码管理 | ||||||
| 
 | 
 | ||||||
| | 特性                                     | Gitea                                            | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | | | 特性                                     | Gitea                                            | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | | ||||||
| |------------------------------------------|-------|------|-----------|-----------|-----------|-----------|--------------| | | ---------------------------------------- | ------------------------------------------------ | ---- | --------- | --------- | --------- | --------- | ------------ | | ||||||
| | 仓库主题描述                             | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | | | 仓库主题描述                             | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | | ||||||
| | 仓库内代码搜索                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | | 仓库内代码搜索                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 全局代码搜索                             | ✓                                                | ✘    | ✓         | ✘         | ✓         | ✓         | ✓            | | | 全局代码搜索                             | ✓                                                | ✘    | ✓         | ✘         | ✓         | ✓         | ✓            | | ||||||
| | Git LFS 2.0                              | ✓     | ✘    | ✓         | ✓         | ✓         | ⁄         | ✓            | | | Git LFS 2.0                              | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 组织里程碑                               | ✘                                                | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 组织里程碑                               | ✘                                                | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | ||||||
| | 细粒度用户角色 (例如 Code, Issues, Wiki) | ✓                                                | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 细粒度用户角色 (例如 Code, Issues, Wiki) | ✓                                                | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | ||||||
| | 提交人的身份验证                         | ⁄                                                | ✘    | ?         | ✓         | ✓         | ✓         | ✘            | | | 提交人的身份验证                         | ⁄                                                | ✘    | ?         | ✓         | ✓         | ✓         | ✘            | | ||||||
| | GPG 签名的提交                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | | GPG 签名的提交                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | SSH 签名的提交                           | ✓                                                | ✘    | ✘         | ✘         | ✘         | ?         | ?            | | | SSH 签名的提交                           | ✓                                                | ✘    | ✘         | ✘         | ✘         | ?         | ?            | | ||||||
| | 拒绝未用通过验证的提交                   | ✓     | ✘    | ✓         | ✓         | ✓         | ✘         | ✓            | | | 拒绝未用通过验证的提交                   | [✓](https://github.com/go-gitea/gitea/pull/9708) | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 仓库活跃度页面                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | | 仓库活跃度页面                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 分支管理                                 | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | | 分支管理                                 | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 建立新分支                               | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | | | 建立新分支                               | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | | ||||||
| | 在线代码编辑                             | ✓                                                | ✓    | ✓         | ✓         | ✓         | ✓         | ✓            | | | 在线代码编辑                             | ✓                                                | ✓    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 提交的统计图表                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | | 提交的统计图表                           | ✓                                                | ✘    | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 模板仓库                                | ✓     | ✘    | ✓         | ✘         | ✓         | ✓         | ✘            | | | 模板仓库                                 | [✓](https://github.com/go-gitea/gitea/pull/8768) | ✘    | ✓         | ✘         | ✓         | ✓         | ✘            | | ||||||
| 
 | 
 | ||||||
| #### Issue 管理 | #### 工单管理 | ||||||
| 
 | 
 | ||||||
| | 特性                | Gitea                                              | Gogs                                          | GitHub EE | GitLab CE                                                               | GitLab EE | BitBucket      | RhodeCode CE | | | 特性                | Gitea                                              | Gogs                                          | GitHub EE | GitLab CE                                                               | GitLab EE | BitBucket      | RhodeCode CE | | ||||||
| |----------------------|-------|------|-----------|-----------|-----------|-----------|--------------| | | ------------------- | -------------------------------------------------- | --------------------------------------------- | --------- | ----------------------------------------------------------------------- | --------- | -------------- | ------------ | | ||||||
| | 跟踪 Issue           | ✓     | ✓    | ✓         | ✓         | ✓         | ✓         | ✘            | | | 工单跟踪            | ✓                                                  | ✓                                             | ✓         | ✓                                                                       | ✓         | ✓ (cloud only) | ✘            | | ||||||
| | Issue 模板           | ✓     | ✓    | ✓         | ✓         | ✓         | ✘         | ✘            | | | 工单模板            | ✓                                                  | ✓                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | 标签                | ✓                                                  | ✓                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | | 标签                | ✓                                                  | ✓                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | 跟踪时间             | ✓     | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | | | 时间跟踪            | ✓                                                  | ✘                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | Issue 可有多个负责人 | ✓     | ✘    | ✓         | ✘         | ✓         | ✘         | ✘            | | | 支持多个负责人      | ✓                                                  | ✘                                             | ✓         | ✘                                                                       | ✓         | ✘              | ✘            | | ||||||
| | 关联的 issues        | ✘     | ✘    | ⁄         | ✘         | ✓         | ✘         | ✘            | | | 关联的工单          | ✘                                                  | ✘                                             | ⁄         | [✓](https://docs.gitlab.com/ce/user/project/issues/related_issues.html) | ✓         | ✘              | ✘            | | ||||||
| | 私密 issues          | ✘     | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 私密工单            | [✘](https://github.com/go-gitea/gitea/issues/3217) | ✘                                             | ✘         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | 评论反馈            | ✓                                                  | ✘                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | | 评论反馈            | ✓                                                  | ✘                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | 锁定讨论            | ✓                                                  | ✘                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | | 锁定讨论            | ✓                                                  | ✘                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | Issue 批量处理       | ✓     | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | | | 工单批处理          | ✓                                                  | ✘                                             | ✓         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | Issue 看板           | ✓     | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 工单看板            | [✓](https://github.com/go-gitea/gitea/pull/8346)   | ✘                                             | ✘         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | 从 issues 创建分支   | ✘     | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 从工单创建分支      | ✘                                                  | ✘                                             | ✘         | ✓                                                                       | ✓         | ✘              | ✘            | | ||||||
| | Issue 搜索           | ✓     | ✘    | ✓         | ✓         | ✓         | ✓         | ✘            | | | 工单搜索            | ✓                                                  | ✘                                             | ✓         | ✓                                                                       | ✓         | ✓              | ✘            | | ||||||
| | 全局 Issue 搜索      | ✘     | ✘    | ✓         | ✓         | ✓         | ✓         | ✘            | | | 工单全局搜索        | [✘](https://github.com/go-gitea/gitea/issues/2434) | ✘                                             | ✓         | ✓                                                                       | ✓         | ✓              | ✘            | | ||||||
| | Issue 依赖           | ✓     | ✘    | ✘         | ✘         | ✘         | ✘         | ✘            | | | 工单依赖关系        | ✓                                                  | ✘                                             | ✘         | ✘                                                                       | ✘         | ✘              | ✘            | | ||||||
| | 通过 Email 创建工单 | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘         | ✘                                                                       | ✓         | ✓              | ✘            | | | 通过 Email 创建工单 | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘         | ✘                                                                       | ✓         | ✓              | ✘            | | ||||||
| | Service Desk | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘ | ✘ | ✘ | ✓ | ✘ | ✘ | | | 服务台              | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘                                             | ✘         | [✓](https://gitlab.com/groups/gitlab-org/-/epics/3103)                  | ✓         | ✘              | ✘            | | ||||||
| 
 | 
 | ||||||
| #### Pull/Merge requests | #### Pull/Merge requests | ||||||
| 
 | 
 | ||||||
| | 特性                                 | Gitea                                              | Gogs | GitHub EE | GitLab CE                                                                         | GitLab EE | BitBucket                                                                | RhodeCode CE | | | 特性                                 | Gitea                                              | Gogs | GitHub EE | GitLab CE                                                                         | GitLab EE | BitBucket                                                                | RhodeCode CE | | ||||||
| |--------------------------------------|-------|------|-----------|-----------|-----------|-----------|--------------| | | ------------------------------------ | -------------------------------------------------- | ---- | --------- | --------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------ | ------------ | | ||||||
| | Pull/Merge requests                  | ✓                                                  | ✓    | ✓         | ✓                                                                                 | ✓         | ✓                                                                        | ✓            | | | Pull/Merge requests                  | ✓                                                  | ✓    | ✓         | ✓                                                                                 | ✓         | ✓                                                                        | ✓            | | ||||||
| | Squash merging                       | ✓     | ✘    | ✓         | ✘         | ✓         | ✓         | ✓            | | | Squash merging                       | ✓                                                  | ✘    | ✓         | [✓](https://docs.gitlab.com/ce/user/project/merge_requests/squash_and_merge.html) | ✓         | ✓                                                                        | ✓            | | ||||||
| | Rebase merging                       | ✓                                                  | ✓    | ✓         | ✘                                                                                 | ⁄         | ✘                                                                        | ✓            | | | Rebase merging                       | ✓                                                  | ✓    | ✓         | ✘                                                                                 | ⁄         | ✘                                                                        | ✓            | | ||||||
| | 评论 Pull/Merge request 中的某行代码 | ✓                                                  | ✘    | ✓         | ✓                                                                                 | ✓         | ✓                                                                        | ✓            | | | 评论 Pull/Merge request 中的某行代码 | ✓                                                  | ✘    | ✓         | ✓                                                                                 | ✓         | ✓                                                                        | ✓            | | ||||||
| | 指定 Pull/Merge request 的审核人     | ✓                                                  | ✘    | ⁄         | ✓                                                                                 | ✓         | ✓                                                                        | ✓            | | | 指定 Pull/Merge request 的审核人     | ✓                                                  | ✘    | ⁄         | ✓                                                                                 | ✓         | ✓                                                                        | ✓            | | ||||||
| | 解决 Merge 冲突                      | ✘     | ✘    | ✓         | ✓         | ✓         | ✓         | ✘            | | | 解决 Merge 冲突                      | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘    | ✓         | ✓                                                                                 | ✓         | ✓                                                                        | ✘            | | ||||||
| | 限制某些用户的 push 和 merge 权限    | ✓                                                  | ✘    | ✓         | ⁄                                                                                 | ✓         | ✓                                                                        | ✓            | | | 限制某些用户的 push 和 merge 权限    | ✓                                                  | ✘    | ✓         | ⁄                                                                                 | ✓         | ✓                                                                        | ✓            | | ||||||
| | 回退某些 commits 或 merge request    | ✘     | ✘    | ✓         | ✓         | ✓         | ✓         | ✘            | | | 回退某些 commits 或 merge request    | [✓](https://github.com/go-gitea/gitea/issues/5158) | ✘    | ✓         | ✓                                                                                 | ✓         | ✓                                                                        | ✘            | | ||||||
| | Pull/Merge requests 模板             | ✓                                                  | ✓    | ✓         | ✓                                                                                 | ✓         | ✘                                                                        | ✘            | | | Pull/Merge requests 模板             | ✓                                                  | ✓    | ✓         | ✓                                                                                 | ✓         | ✘                                                                        | ✘            | | ||||||
| | 查看 Cherry-picking 的更改           | ✘     | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | | | 查看 Cherry-picking 的更改           | [✓](https://github.com/go-gitea/gitea/issues/5158) | ✘    | ✘         | ✓                                                                                 | ✓         | ✘                                                                        | ✘            | | ||||||
| | 下载 Patch                          | ✓     | ✘    | ✓         | ✓         | ✓         | /         | ✘            | | | 下载 Patch                           | ✓                                                  | ✘    | ✓         | ✓                                                                                 | ✓         | [/](https://jira.atlassian.com/plugins/servlet/mobile#issue/BCLOUD-8323) | ✘            | | ||||||
| 
 | 
 | ||||||
| #### 第三方集成 | #### 第三方集成 | ||||||
| 
 | 
 | ||||||
| | 特性                       | Gitea                                              | Gogs                                          | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | | | 特性                       | Gitea                                              | Gogs                                          | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | | ||||||
| |----------------------------|-------|------|-----------|-----------|-----------|-----------|--------------| | | -------------------------- | -------------------------------------------------- | --------------------------------------------- | --------- | --------- | --------- | --------- | ------------ | | ||||||
| | 支持 Webhook               | ✓                                                  | ✓                                             | ✓         | ✓         | ✓         | ✓         | ✓            | | | 支持 Webhook               | ✓                                                  | ✓                                             | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 自定义 Git 钩子            | ✓                                                  | ✓                                             | ✓         | ✓         | ✓         | ✓         | ✓            | | | 自定义 Git 钩子            | ✓                                                  | ✓                                             | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
| | 集成 AD / LDAP             | ✓                                                  | ✓                                             | ✓         | ✓         | ✓         | ✓         | ✓            | | | 集成 AD / LDAP             | ✓                                                  | ✓                                             | ✓         | ✓         | ✓         | ✓         | ✓            | | ||||||
|  |  | ||||||
|  | @ -131,7 +131,8 @@ You can try it out using [the online demo](https://try.gitea.io/). | ||||||
|   - Environment variables |   - Environment variables | ||||||
|   - Command line options |   - Command line options | ||||||
| - Multi-language support ([21 languages](https://github.com/go-gitea/gitea/tree/main/options/locale)) | - Multi-language support ([21 languages](https://github.com/go-gitea/gitea/tree/main/options/locale)) | ||||||
| - [Mermaid](https://mermaidjs.github.io/) Diagram support | - [Mermaid](https://mermaidjs.github.io/) diagrams in Markdown | ||||||
|  | - Math syntax in Markdown | ||||||
| - Mail service | - Mail service | ||||||
|   - Notifications |   - Notifications | ||||||
|   - Registration confirmation |   - Registration confirmation | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -104,6 +104,7 @@ require ( | ||||||
| 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df | 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df | ||||||
| 	gopkg.in/ini.v1 v1.67.0 | 	gopkg.in/ini.v1 v1.67.0 | ||||||
| 	gopkg.in/yaml.v2 v2.4.0 | 	gopkg.in/yaml.v2 v2.4.0 | ||||||
|  | 	gopkg.in/yaml.v3 v3.0.1 | ||||||
| 	mvdan.cc/xurls/v2 v2.4.0 | 	mvdan.cc/xurls/v2 v2.4.0 | ||||||
| 	strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 | 	strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 | ||||||
| 	xorm.io/builder v0.3.11 | 	xorm.io/builder v0.3.11 | ||||||
|  | @ -290,7 +291,6 @@ require ( | ||||||
| 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect | 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect | ||||||
| 	gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect | 	gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect | ||||||
| 	gopkg.in/warnings.v0 v0.1.2 // indirect | 	gopkg.in/warnings.v0 v0.1.2 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect |  | ||||||
| 	sigs.k8s.io/yaml v1.2.0 // indirect | 	sigs.k8s.io/yaml v1.2.0 // indirect | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -63,34 +63,32 @@ func (repo *Repository) IsBranchExist(name string) bool { | ||||||
| // GetBranchNames returns branches from the repository, skipping skip initial branches and | // GetBranchNames returns branches from the repository, skipping skip initial branches and | ||||||
| // returning at most limit branches, or all branches if limit is 0. | // returning at most limit branches, or all branches if limit is 0. | ||||||
| func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { | func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { | ||||||
| 	return callShowRef(repo.Ctx, repo.Path, BranchPrefix, "--heads", skip, limit) | 	return callShowRef(repo.Ctx, repo.Path, BranchPrefix, []string{BranchPrefix, "--sort=-committerdate"}, skip, limit) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // WalkReferences walks all the references from the repository | // WalkReferences walks all the references from the repository | ||||||
| func WalkReferences(ctx context.Context, repoPath string, walkfn func(sha1, refname string) error) (int, error) { | func WalkReferences(ctx context.Context, repoPath string, walkfn func(sha1, refname string) error) (int, error) { | ||||||
| 	return walkShowRef(ctx, repoPath, "", 0, 0, walkfn) | 	return walkShowRef(ctx, repoPath, nil, 0, 0, walkfn) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // WalkReferences walks all the references from the repository | // WalkReferences walks all the references from the repository | ||||||
| // refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty. | // refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty. | ||||||
| func (repo *Repository) WalkReferences(refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { | func (repo *Repository) WalkReferences(refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { | ||||||
| 	var arg string | 	var args []string | ||||||
| 	switch refType { | 	switch refType { | ||||||
| 	case ObjectTag: | 	case ObjectTag: | ||||||
| 		arg = "--tags" | 		args = []string{TagPrefix, "--sort=-taggerdate"} | ||||||
| 	case ObjectBranch: | 	case ObjectBranch: | ||||||
| 		arg = "--heads" | 		args = []string{BranchPrefix, "--sort=-committerdate"} | ||||||
| 	default: |  | ||||||
| 		arg = "" |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return walkShowRef(repo.Ctx, repo.Path, arg, skip, limit, walkfn) | 	return walkShowRef(repo.Ctx, repo.Path, args, skip, limit, walkfn) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // callShowRef return refs, if limit = 0 it will not limit | // callShowRef return refs, if limit = 0 it will not limit | ||||||
| func callShowRef(ctx context.Context, repoPath, prefix, arg string, skip, limit int) (branchNames []string, countAll int, err error) { | func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs []string, skip, limit int) (branchNames []string, countAll int, err error) { | ||||||
| 	countAll, err = walkShowRef(ctx, repoPath, arg, skip, limit, func(_, branchName string) error { | 	countAll, err = walkShowRef(ctx, repoPath, extraArgs, skip, limit, func(_, branchName string) error { | ||||||
| 		branchName = strings.TrimPrefix(branchName, prefix) | 		branchName = strings.TrimPrefix(branchName, trimPrefix) | ||||||
| 		branchNames = append(branchNames, branchName) | 		branchNames = append(branchNames, branchName) | ||||||
| 
 | 
 | ||||||
| 		return nil | 		return nil | ||||||
|  | @ -98,7 +96,7 @@ func callShowRef(ctx context.Context, repoPath, prefix, arg string, skip, limit | ||||||
| 	return branchNames, countAll, err | 	return branchNames, countAll, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func walkShowRef(ctx context.Context, repoPath, arg string, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) { | func walkShowRef(ctx context.Context, repoPath string, extraArgs []string, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) { | ||||||
| 	stdoutReader, stdoutWriter := io.Pipe() | 	stdoutReader, stdoutWriter := io.Pipe() | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		_ = stdoutReader.Close() | 		_ = stdoutReader.Close() | ||||||
|  | @ -107,10 +105,8 @@ func walkShowRef(ctx context.Context, repoPath, arg string, skip, limit int, wal | ||||||
| 
 | 
 | ||||||
| 	go func() { | 	go func() { | ||||||
| 		stderrBuilder := &strings.Builder{} | 		stderrBuilder := &strings.Builder{} | ||||||
| 		args := []string{"show-ref"} | 		args := []string{"for-each-ref", "--format=%(objectname) %(refname)"} | ||||||
| 		if arg != "" { | 		args = append(args, extraArgs...) | ||||||
| 			args = append(args, arg) |  | ||||||
| 		} |  | ||||||
| 		err := NewCommand(ctx, args...).Run(&RunOpts{ | 		err := NewCommand(ctx, args...).Run(&RunOpts{ | ||||||
| 			Dir:    repoPath, | 			Dir:    repoPath, | ||||||
| 			Stdout: stdoutWriter, | 			Stdout: stdoutWriter, | ||||||
|  | @ -194,7 +190,7 @@ func walkShowRef(ctx context.Context, repoPath, arg string, skip, limit int, wal | ||||||
| // GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash | // GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash | ||||||
| func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { | func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { | ||||||
| 	var revList []string | 	var revList []string | ||||||
| 	_, err := walkShowRef(repo.Ctx, repo.Path, "", 0, 0, func(walkSha, refname string) error { | 	_, err := walkShowRef(repo.Ctx, repo.Path, nil, 0, 0, func(walkSha, refname string) error { | ||||||
| 		if walkSha == sha && strings.HasPrefix(refname, prefix) { | 		if walkSha == sha && strings.HasPrefix(refname, prefix) { | ||||||
| 			revList = append(revList, refname) | 			revList = append(revList, refname) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -22,14 +22,14 @@ func TestRepository_GetBranches(t *testing.T) { | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Len(t, branches, 2) | 	assert.Len(t, branches, 2) | ||||||
| 	assert.EqualValues(t, 3, countAll) | 	assert.EqualValues(t, 3, countAll) | ||||||
| 	assert.ElementsMatch(t, []string{"branch1", "branch2"}, branches) | 	assert.ElementsMatch(t, []string{"master", "branch2"}, branches) | ||||||
| 
 | 
 | ||||||
| 	branches, countAll, err = bareRepo1.GetBranchNames(0, 0) | 	branches, countAll, err = bareRepo1.GetBranchNames(0, 0) | ||||||
| 
 | 
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Len(t, branches, 3) | 	assert.Len(t, branches, 3) | ||||||
| 	assert.EqualValues(t, 3, countAll) | 	assert.EqualValues(t, 3, countAll) | ||||||
| 	assert.ElementsMatch(t, []string{"branch1", "branch2", "master"}, branches) | 	assert.ElementsMatch(t, []string{"master", "branch2", "branch1"}, branches) | ||||||
| 
 | 
 | ||||||
| 	branches, countAll, err = bareRepo1.GetBranchNames(5, 1) | 	branches, countAll, err = bareRepo1.GetBranchNames(5, 1) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ func (repo *Repository) IsTagExist(name string) bool { | ||||||
| // GetTags returns all tags of the repository. | // GetTags returns all tags of the repository. | ||||||
| // returning at most limit tags, or all if limit is 0. | // returning at most limit tags, or all if limit is 0. | ||||||
| func (repo *Repository) GetTags(skip, limit int) (tags []string, err error) { | func (repo *Repository) GetTags(skip, limit int) (tags []string, err error) { | ||||||
| 	tags, _, err = callShowRef(repo.Ctx, repo.Path, TagPrefix, "--tags", skip, limit) | 	tags, _, err = callShowRef(repo.Ctx, repo.Path, TagPrefix, []string{TagPrefix, "--sort=-taggerdate"}, skip, limit) | ||||||
| 	return tags, err | 	return tags, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										84
									
								
								modules/markup/markdown/convertyaml.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								modules/markup/markdown/convertyaml.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | ||||||
|  | // 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 markdown | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/yuin/goldmark/ast" | ||||||
|  | 	east "github.com/yuin/goldmark/extension/ast" | ||||||
|  | 	"gopkg.in/yaml.v3" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func nodeToTable(meta *yaml.Node) ast.Node { | ||||||
|  | 	for { | ||||||
|  | 		if meta == nil { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		switch meta.Kind { | ||||||
|  | 		case yaml.DocumentNode: | ||||||
|  | 			meta = meta.Content[0] | ||||||
|  | 			continue | ||||||
|  | 		default: | ||||||
|  | 		} | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  | 	switch meta.Kind { | ||||||
|  | 	case yaml.MappingNode: | ||||||
|  | 		return mappingNodeToTable(meta) | ||||||
|  | 	case yaml.SequenceNode: | ||||||
|  | 		return sequenceNodeToTable(meta) | ||||||
|  | 	default: | ||||||
|  | 		return ast.NewString([]byte(meta.Value)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func mappingNodeToTable(meta *yaml.Node) ast.Node { | ||||||
|  | 	table := east.NewTable() | ||||||
|  | 	alignments := []east.Alignment{} | ||||||
|  | 	for i := 0; i < len(meta.Content); i += 2 { | ||||||
|  | 		alignments = append(alignments, east.AlignNone) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	headerRow := east.NewTableRow(alignments) | ||||||
|  | 	valueRow := east.NewTableRow(alignments) | ||||||
|  | 	for i := 0; i < len(meta.Content); i += 2 { | ||||||
|  | 		cell := east.NewTableCell() | ||||||
|  | 
 | ||||||
|  | 		cell.AppendChild(cell, nodeToTable(meta.Content[i])) | ||||||
|  | 		headerRow.AppendChild(headerRow, cell) | ||||||
|  | 
 | ||||||
|  | 		if i+1 < len(meta.Content) { | ||||||
|  | 			cell = east.NewTableCell() | ||||||
|  | 			cell.AppendChild(cell, nodeToTable(meta.Content[i+1])) | ||||||
|  | 			valueRow.AppendChild(valueRow, cell) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	table.AppendChild(table, east.NewTableHeader(headerRow)) | ||||||
|  | 	table.AppendChild(table, valueRow) | ||||||
|  | 	return table | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func sequenceNodeToTable(meta *yaml.Node) ast.Node { | ||||||
|  | 	table := east.NewTable() | ||||||
|  | 	alignments := []east.Alignment{east.AlignNone} | ||||||
|  | 	for _, item := range meta.Content { | ||||||
|  | 		row := east.NewTableRow(alignments) | ||||||
|  | 		cell := east.NewTableCell() | ||||||
|  | 		cell.AppendChild(cell, nodeToTable(item)) | ||||||
|  | 		row.AppendChild(row, cell) | ||||||
|  | 		table.AppendChild(table, row) | ||||||
|  | 	} | ||||||
|  | 	return table | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func nodeToDetails(meta *yaml.Node, icon string) ast.Node { | ||||||
|  | 	details := NewDetails() | ||||||
|  | 	summary := NewSummary() | ||||||
|  | 	summary.AppendChild(summary, NewIcon(icon)) | ||||||
|  | 	details.AppendChild(details, summary) | ||||||
|  | 	details.AppendChild(details, nodeToTable(meta)) | ||||||
|  | 
 | ||||||
|  | 	return details | ||||||
|  | } | ||||||
|  | @ -15,7 +15,6 @@ import ( | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	giteautil "code.gitea.io/gitea/modules/util" | 	giteautil "code.gitea.io/gitea/modules/util" | ||||||
| 
 | 
 | ||||||
| 	meta "github.com/yuin/goldmark-meta" |  | ||||||
| 	"github.com/yuin/goldmark/ast" | 	"github.com/yuin/goldmark/ast" | ||||||
| 	east "github.com/yuin/goldmark/extension/ast" | 	east "github.com/yuin/goldmark/extension/ast" | ||||||
| 	"github.com/yuin/goldmark/parser" | 	"github.com/yuin/goldmark/parser" | ||||||
|  | @ -32,20 +31,12 @@ type ASTTransformer struct{} | ||||||
| 
 | 
 | ||||||
| // Transform transforms the given AST tree. | // Transform transforms the given AST tree. | ||||||
| func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { | func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { | ||||||
| 	metaData := meta.GetItems(pc) |  | ||||||
| 	firstChild := node.FirstChild() | 	firstChild := node.FirstChild() | ||||||
| 	createTOC := false | 	createTOC := false | ||||||
| 	ctx := pc.Get(renderContextKey).(*markup.RenderContext) | 	ctx := pc.Get(renderContextKey).(*markup.RenderContext) | ||||||
| 	rc := &RenderConfig{ | 	rc := pc.Get(renderConfigKey).(*RenderConfig) | ||||||
| 		Meta: "table", | 	if rc.yamlNode != nil { | ||||||
| 		Icon: "table", | 		metaNode := rc.toMetaNode() | ||||||
| 		Lang: "", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if metaData != nil { |  | ||||||
| 		rc.ToRenderConfig(metaData) |  | ||||||
| 
 |  | ||||||
| 		metaNode := rc.toMetaNode(metaData) |  | ||||||
| 		if metaNode != nil { | 		if metaNode != nil { | ||||||
| 			node.InsertBefore(node, firstChild, metaNode) | 			node.InsertBefore(node, firstChild, metaNode) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/common" | 	"code.gitea.io/gitea/modules/markup/common" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup/markdown/math" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	giteautil "code.gitea.io/gitea/modules/util" | 	giteautil "code.gitea.io/gitea/modules/util" | ||||||
| 
 | 
 | ||||||
|  | @ -38,6 +39,7 @@ var ( | ||||||
| 	isWikiKey        = parser.NewContextKey() | 	isWikiKey        = parser.NewContextKey() | ||||||
| 	renderMetasKey   = parser.NewContextKey() | 	renderMetasKey   = parser.NewContextKey() | ||||||
| 	renderContextKey = parser.NewContextKey() | 	renderContextKey = parser.NewContextKey() | ||||||
|  | 	renderConfigKey  = parser.NewContextKey() | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type limitWriter struct { | type limitWriter struct { | ||||||
|  | @ -98,7 +100,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) | ||||||
| 							languageStr := string(language) | 							languageStr := string(language) | ||||||
| 
 | 
 | ||||||
| 							preClasses := []string{"code-block"} | 							preClasses := []string{"code-block"} | ||||||
| 							if languageStr == "mermaid" { | 							if languageStr == "mermaid" || languageStr == "math" { | ||||||
| 								preClasses = append(preClasses, "is-loading") | 								preClasses = append(preClasses, "is-loading") | ||||||
| 							} | 							} | ||||||
| 
 | 
 | ||||||
|  | @ -120,6 +122,9 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) | ||||||
| 						} | 						} | ||||||
| 					}), | 					}), | ||||||
| 				), | 				), | ||||||
|  | 				math.NewExtension( | ||||||
|  | 					math.Enabled(setting.Markdown.EnableMath), | ||||||
|  | 				), | ||||||
| 				meta.Meta, | 				meta.Meta, | ||||||
| 			), | 			), | ||||||
| 			goldmark.WithParserOptions( | 			goldmark.WithParserOptions( | ||||||
|  | @ -167,7 +172,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) | ||||||
| 		log.Error("Unable to ReadAll: %v", err) | 		log.Error("Unable to ReadAll: %v", err) | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if err := converter.Convert(giteautil.NormalizeEOL(buf), lw, parser.WithContext(pc)); err != nil { | 	buf = giteautil.NormalizeEOL(buf) | ||||||
|  | 
 | ||||||
|  | 	rc := &RenderConfig{ | ||||||
|  | 		Meta: "table", | ||||||
|  | 		Icon: "table", | ||||||
|  | 		Lang: "", | ||||||
|  | 	} | ||||||
|  | 	buf, _ = ExtractMetadataBytes(buf, rc) | ||||||
|  | 
 | ||||||
|  | 	pc.Set(renderConfigKey, rc) | ||||||
|  | 
 | ||||||
|  | 	if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil { | ||||||
| 		log.Error("Unable to render: %v", err) | 		log.Error("Unable to render: %v", err) | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
							
								
								
									
										42
									
								
								modules/markup/markdown/math/block_node.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								modules/markup/markdown/math/block_node.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | ||||||
|  | // 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 math | ||||||
|  | 
 | ||||||
|  | import "github.com/yuin/goldmark/ast" | ||||||
|  | 
 | ||||||
|  | // Block represents a display math block e.g. $$...$$ or \[...\] | ||||||
|  | type Block struct { | ||||||
|  | 	ast.BaseBlock | ||||||
|  | 	Dollars bool | ||||||
|  | 	Indent  int | ||||||
|  | 	Closed  bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // KindBlock is the node kind for math blocks | ||||||
|  | var KindBlock = ast.NewNodeKind("MathBlock") | ||||||
|  | 
 | ||||||
|  | // NewBlock creates a new math Block | ||||||
|  | func NewBlock(dollars bool, indent int) *Block { | ||||||
|  | 	return &Block{ | ||||||
|  | 		Dollars: dollars, | ||||||
|  | 		Indent:  indent, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Dump dumps the block to a string | ||||||
|  | func (n *Block) Dump(source []byte, level int) { | ||||||
|  | 	m := map[string]string{} | ||||||
|  | 	ast.DumpHelper(n, source, level, m, nil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Kind returns KindBlock for math Blocks | ||||||
|  | func (n *Block) Kind() ast.NodeKind { | ||||||
|  | 	return KindBlock | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsRaw returns true as this block should not be processed further | ||||||
|  | func (n *Block) IsRaw() bool { | ||||||
|  | 	return true | ||||||
|  | } | ||||||
							
								
								
									
										123
									
								
								modules/markup/markdown/math/block_parser.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								modules/markup/markdown/math/block_parser.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,123 @@ | ||||||
|  | // 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 math | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 
 | ||||||
|  | 	"github.com/yuin/goldmark/ast" | ||||||
|  | 	"github.com/yuin/goldmark/parser" | ||||||
|  | 	"github.com/yuin/goldmark/text" | ||||||
|  | 	"github.com/yuin/goldmark/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type blockParser struct { | ||||||
|  | 	parseDollars bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewBlockParser creates a new math BlockParser | ||||||
|  | func NewBlockParser(parseDollarBlocks bool) parser.BlockParser { | ||||||
|  | 	return &blockParser{ | ||||||
|  | 		parseDollars: parseDollarBlocks, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Open parses the current line and returns a result of parsing. | ||||||
|  | func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { | ||||||
|  | 	line, segment := reader.PeekLine() | ||||||
|  | 	pos := pc.BlockOffset() | ||||||
|  | 	if pos == -1 || len(line[pos:]) < 2 { | ||||||
|  | 		return nil, parser.NoChildren | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dollars := false | ||||||
|  | 	if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' { | ||||||
|  | 		dollars = true | ||||||
|  | 	} else if line[pos] != '\\' || line[pos+1] != '[' { | ||||||
|  | 		return nil, parser.NoChildren | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	node := NewBlock(dollars, pos) | ||||||
|  | 
 | ||||||
|  | 	// Now we need to check if the ending block is on the segment... | ||||||
|  | 	endBytes := []byte{'\\', ']'} | ||||||
|  | 	if dollars { | ||||||
|  | 		endBytes = []byte{'$', '$'} | ||||||
|  | 	} | ||||||
|  | 	idx := bytes.Index(line[pos+2:], endBytes) | ||||||
|  | 	if idx >= 0 { | ||||||
|  | 		segment.Stop = segment.Start + idx + 2 | ||||||
|  | 		reader.Advance(segment.Len() - 1) | ||||||
|  | 		segment.Start += 2 | ||||||
|  | 		node.Lines().Append(segment) | ||||||
|  | 		node.Closed = true | ||||||
|  | 		return node, parser.Close | parser.NoChildren | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	reader.Advance(segment.Len() - 1) | ||||||
|  | 	segment.Start += 2 | ||||||
|  | 	node.Lines().Append(segment) | ||||||
|  | 	return node, parser.NoChildren | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Continue parses the current line and returns a result of parsing. | ||||||
|  | func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { | ||||||
|  | 	block := node.(*Block) | ||||||
|  | 	if block.Closed { | ||||||
|  | 		return parser.Close | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	line, segment := reader.PeekLine() | ||||||
|  | 	w, pos := util.IndentWidth(line, 0) | ||||||
|  | 	if w < 4 { | ||||||
|  | 		if block.Dollars { | ||||||
|  | 			i := pos | ||||||
|  | 			for ; i < len(line) && line[i] == '$'; i++ { | ||||||
|  | 			} | ||||||
|  | 			length := i - pos | ||||||
|  | 			if length >= 2 && util.IsBlank(line[i:]) { | ||||||
|  | 				reader.Advance(segment.Stop - segment.Start - segment.Padding) | ||||||
|  | 				block.Closed = true | ||||||
|  | 				return parser.Close | ||||||
|  | 			} | ||||||
|  | 		} else if len(line[pos:]) > 1 && line[pos] == '\\' && line[pos+1] == ']' && util.IsBlank(line[pos+2:]) { | ||||||
|  | 			reader.Advance(segment.Stop - segment.Start - segment.Padding) | ||||||
|  | 			block.Closed = true | ||||||
|  | 			return parser.Close | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pos, padding := util.IndentPosition(line, 0, block.Indent) | ||||||
|  | 	seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding) | ||||||
|  | 	node.Lines().Append(seg) | ||||||
|  | 	reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) | ||||||
|  | 	return parser.Continue | parser.NoChildren | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Close will be called when the parser returns Close. | ||||||
|  | func (b *blockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { | ||||||
|  | 	// noop | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CanInterruptParagraph returns true if the parser can interrupt paragraphs, | ||||||
|  | // otherwise false. | ||||||
|  | func (b *blockParser) CanInterruptParagraph() bool { | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CanAcceptIndentedLine returns true if the parser can open new node when | ||||||
|  | // the given line is being indented more than 3 spaces. | ||||||
|  | func (b *blockParser) CanAcceptIndentedLine() bool { | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Trigger returns a list of characters that triggers Parse method of | ||||||
|  | // this parser. | ||||||
|  | // If Trigger returns a nil, Open will be called with any lines. | ||||||
|  | // | ||||||
|  | // We leave this as nil as our parse method is quick enough | ||||||
|  | func (b *blockParser) Trigger() []byte { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								modules/markup/markdown/math/block_renderer.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								modules/markup/markdown/math/block_renderer.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | // 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 math | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	gast "github.com/yuin/goldmark/ast" | ||||||
|  | 	"github.com/yuin/goldmark/renderer" | ||||||
|  | 	"github.com/yuin/goldmark/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // BlockRenderer represents a renderer for math Blocks | ||||||
|  | type BlockRenderer struct{} | ||||||
|  | 
 | ||||||
|  | // NewBlockRenderer creates a new renderer for math Blocks | ||||||
|  | func NewBlockRenderer() renderer.NodeRenderer { | ||||||
|  | 	return &BlockRenderer{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RegisterFuncs registers the renderer for math Blocks | ||||||
|  | func (r *BlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { | ||||||
|  | 	reg.Register(KindBlock, r.renderBlock) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) { | ||||||
|  | 	l := n.Lines().Len() | ||||||
|  | 	for i := 0; i < l; i++ { | ||||||
|  | 		line := n.Lines().At(i) | ||||||
|  | 		_, _ = w.Write(util.EscapeHTML(line.Value(source))) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { | ||||||
|  | 	n := node.(*Block) | ||||||
|  | 	if entering { | ||||||
|  | 		_, _ = w.WriteString(`<pre class="code-block is-loading"><code class="chroma language-math display">`) | ||||||
|  | 		r.writeLines(w, source, n) | ||||||
|  | 	} else { | ||||||
|  | 		_, _ = w.WriteString(`</code></pre>` + "\n") | ||||||
|  | 	} | ||||||
|  | 	return gast.WalkContinue, nil | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								modules/markup/markdown/math/inline_node.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								modules/markup/markdown/math/inline_node.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | ||||||
|  | // 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 math | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/yuin/goldmark/ast" | ||||||
|  | 	"github.com/yuin/goldmark/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Inline represents inline math e.g. $...$ or \(...\) | ||||||
|  | type Inline struct { | ||||||
|  | 	ast.BaseInline | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Inline implements Inline.Inline. | ||||||
|  | func (n *Inline) Inline() {} | ||||||
|  | 
 | ||||||
|  | // IsBlank returns if this inline node is empty | ||||||
|  | func (n *Inline) IsBlank(source []byte) bool { | ||||||
|  | 	for c := n.FirstChild(); c != nil; c = c.NextSibling() { | ||||||
|  | 		text := c.(*ast.Text).Segment | ||||||
|  | 		if !util.IsBlank(text.Value(source)) { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Dump renders this inline math as debug | ||||||
|  | func (n *Inline) Dump(source []byte, level int) { | ||||||
|  | 	ast.DumpHelper(n, source, level, nil, nil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // KindInline is the kind for math inline | ||||||
|  | var KindInline = ast.NewNodeKind("MathInline") | ||||||
|  | 
 | ||||||
|  | // Kind returns KindInline | ||||||
|  | func (n *Inline) Kind() ast.NodeKind { | ||||||
|  | 	return KindInline | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewInline creates a new ast math inline node | ||||||
|  | func NewInline() *Inline { | ||||||
|  | 	return &Inline{ | ||||||
|  | 		BaseInline: ast.BaseInline{}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										99
									
								
								modules/markup/markdown/math/inline_parser.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								modules/markup/markdown/math/inline_parser.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | ||||||
|  | // 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 math | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 
 | ||||||
|  | 	"github.com/yuin/goldmark/ast" | ||||||
|  | 	"github.com/yuin/goldmark/parser" | ||||||
|  | 	"github.com/yuin/goldmark/text" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type inlineParser struct { | ||||||
|  | 	start []byte | ||||||
|  | 	end   []byte | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var defaultInlineDollarParser = &inlineParser{ | ||||||
|  | 	start: []byte{'$'}, | ||||||
|  | 	end:   []byte{'$'}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewInlineDollarParser returns a new inline parser | ||||||
|  | func NewInlineDollarParser() parser.InlineParser { | ||||||
|  | 	return defaultInlineDollarParser | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var defaultInlineBracketParser = &inlineParser{ | ||||||
|  | 	start: []byte{'\\', '('}, | ||||||
|  | 	end:   []byte{'\\', ')'}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewInlineDollarParser returns a new inline parser | ||||||
|  | func NewInlineBracketParser() parser.InlineParser { | ||||||
|  | 	return defaultInlineBracketParser | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Trigger triggers this parser on $ | ||||||
|  | func (parser *inlineParser) Trigger() []byte { | ||||||
|  | 	return parser.start[0:1] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func isAlphanumeric(b byte) bool { | ||||||
|  | 	// Github only cares about 0-9A-Za-z | ||||||
|  | 	return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Parse parses the current line and returns a result of parsing. | ||||||
|  | func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { | ||||||
|  | 	line, _ := block.PeekLine() | ||||||
|  | 	opener := bytes.Index(line, parser.start) | ||||||
|  | 	if opener < 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if opener != 0 && isAlphanumeric(line[opener-1]) { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opener += len(parser.start) | ||||||
|  | 	ender := bytes.Index(line[opener:], parser.end) | ||||||
|  | 	if ender < 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if len(line) > opener+ender+len(parser.end) && isAlphanumeric(line[opener+ender+len(parser.end)]) { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	block.Advance(opener) | ||||||
|  | 	_, pos := block.Position() | ||||||
|  | 	node := NewInline() | ||||||
|  | 	segment := pos.WithStop(pos.Start + ender) | ||||||
|  | 	node.AppendChild(node, ast.NewRawTextSegment(segment)) | ||||||
|  | 	block.Advance(ender + len(parser.end)) | ||||||
|  | 
 | ||||||
|  | 	trimBlock(node, block) | ||||||
|  | 	return node | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func trimBlock(node *Inline, block text.Reader) { | ||||||
|  | 	if node.IsBlank(block.Source()) { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// trim first space and last space | ||||||
|  | 	first := node.FirstChild().(*ast.Text) | ||||||
|  | 	if !(!first.Segment.IsEmpty() && block.Source()[first.Segment.Start] == ' ') { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	last := node.LastChild().(*ast.Text) | ||||||
|  | 	if !(!last.Segment.IsEmpty() && block.Source()[last.Segment.Stop-1] == ' ') { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	first.Segment = first.Segment.WithStart(first.Segment.Start + 1) | ||||||
|  | 	last.Segment = last.Segment.WithStop(last.Segment.Stop - 1) | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								modules/markup/markdown/math/inline_renderer.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								modules/markup/markdown/math/inline_renderer.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | // 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 math | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 
 | ||||||
|  | 	"github.com/yuin/goldmark/ast" | ||||||
|  | 	"github.com/yuin/goldmark/renderer" | ||||||
|  | 	"github.com/yuin/goldmark/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // InlineRenderer is an inline renderer | ||||||
|  | type InlineRenderer struct{} | ||||||
|  | 
 | ||||||
|  | // NewInlineRenderer returns a new renderer for inline math | ||||||
|  | func NewInlineRenderer() renderer.NodeRenderer { | ||||||
|  | 	return &InlineRenderer{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { | ||||||
|  | 	if entering { | ||||||
|  | 		_, _ = w.WriteString(`<code class="language-math is-loading">`) | ||||||
|  | 		for c := n.FirstChild(); c != nil; c = c.NextSibling() { | ||||||
|  | 			segment := c.(*ast.Text).Segment | ||||||
|  | 			value := util.EscapeHTML(segment.Value(source)) | ||||||
|  | 			if bytes.HasSuffix(value, []byte("\n")) { | ||||||
|  | 				_, _ = w.Write(value[:len(value)-1]) | ||||||
|  | 				if c != n.LastChild() { | ||||||
|  | 					_, _ = w.Write([]byte(" ")) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				_, _ = w.Write(value) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return ast.WalkSkipChildren, nil | ||||||
|  | 	} | ||||||
|  | 	_, _ = w.WriteString(`</code>`) | ||||||
|  | 	return ast.WalkContinue, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RegisterFuncs registers the renderer for inline math nodes | ||||||
|  | func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { | ||||||
|  | 	reg.Register(KindInline, r.renderInline) | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								modules/markup/markdown/math/math.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								modules/markup/markdown/math/math.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,108 @@ | ||||||
|  | // 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 math | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/yuin/goldmark" | ||||||
|  | 	"github.com/yuin/goldmark/parser" | ||||||
|  | 	"github.com/yuin/goldmark/renderer" | ||||||
|  | 	"github.com/yuin/goldmark/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Extension is a math extension | ||||||
|  | type Extension struct { | ||||||
|  | 	enabled           bool | ||||||
|  | 	parseDollarInline bool | ||||||
|  | 	parseDollarBlock  bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Option is the interface Options should implement | ||||||
|  | type Option interface { | ||||||
|  | 	SetOption(e *Extension) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type extensionFunc func(e *Extension) | ||||||
|  | 
 | ||||||
|  | func (fn extensionFunc) SetOption(e *Extension) { | ||||||
|  | 	fn(e) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Enabled enables or disables this extension | ||||||
|  | func Enabled(enable ...bool) Option { | ||||||
|  | 	value := true | ||||||
|  | 	if len(enable) > 0 { | ||||||
|  | 		value = enable[0] | ||||||
|  | 	} | ||||||
|  | 	return extensionFunc(func(e *Extension) { | ||||||
|  | 		e.enabled = value | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WithInlineDollarParser enables or disables the parsing of $...$ | ||||||
|  | func WithInlineDollarParser(enable ...bool) Option { | ||||||
|  | 	value := true | ||||||
|  | 	if len(enable) > 0 { | ||||||
|  | 		value = enable[0] | ||||||
|  | 	} | ||||||
|  | 	return extensionFunc(func(e *Extension) { | ||||||
|  | 		e.parseDollarInline = value | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WithBlockDollarParser enables or disables the parsing of $$...$$ | ||||||
|  | func WithBlockDollarParser(enable ...bool) Option { | ||||||
|  | 	value := true | ||||||
|  | 	if len(enable) > 0 { | ||||||
|  | 		value = enable[0] | ||||||
|  | 	} | ||||||
|  | 	return extensionFunc(func(e *Extension) { | ||||||
|  | 		e.parseDollarBlock = value | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Math represents a math extension with default rendered delimiters | ||||||
|  | var Math = &Extension{ | ||||||
|  | 	enabled:           true, | ||||||
|  | 	parseDollarBlock:  true, | ||||||
|  | 	parseDollarInline: true, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewExtension creates a new math extension with the provided options | ||||||
|  | func NewExtension(opts ...Option) *Extension { | ||||||
|  | 	r := &Extension{ | ||||||
|  | 		enabled:           true, | ||||||
|  | 		parseDollarBlock:  true, | ||||||
|  | 		parseDollarInline: true, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o.SetOption(r) | ||||||
|  | 	} | ||||||
|  | 	return r | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Extend extends goldmark with our parsers and renderers | ||||||
|  | func (e *Extension) Extend(m goldmark.Markdown) { | ||||||
|  | 	if !e.enabled { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	m.Parser().AddOptions(parser.WithBlockParsers( | ||||||
|  | 		util.Prioritized(NewBlockParser(e.parseDollarBlock), 701), | ||||||
|  | 	)) | ||||||
|  | 
 | ||||||
|  | 	inlines := []util.PrioritizedValue{ | ||||||
|  | 		util.Prioritized(NewInlineBracketParser(), 501), | ||||||
|  | 	} | ||||||
|  | 	if e.parseDollarInline { | ||||||
|  | 		inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 501)) | ||||||
|  | 	} | ||||||
|  | 	m.Parser().AddOptions(parser.WithInlineParsers(inlines...)) | ||||||
|  | 
 | ||||||
|  | 	m.Renderer().AddOptions(renderer.WithNodeRenderers( | ||||||
|  | 		util.Prioritized(NewBlockRenderer(), 501), | ||||||
|  | 		util.Prioritized(NewInlineRenderer(), 502), | ||||||
|  | 	)) | ||||||
|  | } | ||||||
|  | @ -5,47 +5,101 @@ | ||||||
| package markdown | package markdown | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"strings" | 	"unicode" | ||||||
|  | 	"unicode/utf8" | ||||||
| 
 | 
 | ||||||
| 	"gopkg.in/yaml.v2" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"gopkg.in/yaml.v3" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func isYAMLSeparator(line string) bool { | func isYAMLSeparator(line []byte) bool { | ||||||
| 	line = strings.TrimSpace(line) | 	idx := 0 | ||||||
| 	for i := 0; i < len(line); i++ { | 	for ; idx < len(line); idx++ { | ||||||
| 		if line[i] != '-' { | 		if line[idx] >= utf8.RuneSelf { | ||||||
|  | 			r, sz := utf8.DecodeRune(line[idx:]) | ||||||
|  | 			if !unicode.IsSpace(r) { | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  | 			idx += sz | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if line[idx] != ' ' { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	dashCount := 0 | ||||||
|  | 	for ; idx < len(line); idx++ { | ||||||
|  | 		if line[idx] != '-' { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		dashCount++ | ||||||
|  | 	} | ||||||
|  | 	if dashCount < 3 { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	for ; idx < len(line); idx++ { | ||||||
|  | 		if line[idx] >= utf8.RuneSelf { | ||||||
|  | 			r, sz := utf8.DecodeRune(line[idx:]) | ||||||
|  | 			if !unicode.IsSpace(r) { | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  | 			idx += sz | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if line[idx] != ' ' { | ||||||
| 			return false | 			return false | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return len(line) > 2 | 	return true | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ExtractMetadata consumes a markdown file, parses YAML frontmatter, | // ExtractMetadata consumes a markdown file, parses YAML frontmatter, | ||||||
| // and returns the frontmatter metadata separated from the markdown content | // and returns the frontmatter metadata separated from the markdown content | ||||||
| func ExtractMetadata(contents string, out interface{}) (string, error) { | func ExtractMetadata(contents string, out interface{}) (string, error) { | ||||||
| 	var front, body []string | 	body, err := ExtractMetadataBytes([]byte(contents), out) | ||||||
| 	lines := strings.Split(contents, "\n") | 	return string(body), err | ||||||
| 	for idx, line := range lines { | } | ||||||
| 		if idx == 0 { | 
 | ||||||
| 			// First line has to be a separator | // ExtractMetadata consumes a markdown file, parses YAML frontmatter, | ||||||
|  | // and returns the frontmatter metadata separated from the markdown content | ||||||
|  | func ExtractMetadataBytes(contents []byte, out interface{}) ([]byte, error) { | ||||||
|  | 	var front, body []byte | ||||||
|  | 
 | ||||||
|  | 	start, end := 0, len(contents) | ||||||
|  | 	idx := bytes.IndexByte(contents[start:], '\n') | ||||||
|  | 	if idx >= 0 { | ||||||
|  | 		end = start + idx | ||||||
|  | 	} | ||||||
|  | 	line := contents[start:end] | ||||||
|  | 
 | ||||||
| 	if !isYAMLSeparator(line) { | 	if !isYAMLSeparator(line) { | ||||||
| 				return "", errors.New("frontmatter must start with a separator line") | 		return contents, errors.New("frontmatter must start with a separator line") | ||||||
| 	} | 	} | ||||||
| 			continue | 	frontMatterStart := end + 1 | ||||||
|  | 	for start = frontMatterStart; start < len(contents); start = end + 1 { | ||||||
|  | 		end = len(contents) | ||||||
|  | 		idx := bytes.IndexByte(contents[start:], '\n') | ||||||
|  | 		if idx >= 0 { | ||||||
|  | 			end = start + idx | ||||||
| 		} | 		} | ||||||
|  | 		line := contents[start:end] | ||||||
| 		if isYAMLSeparator(line) { | 		if isYAMLSeparator(line) { | ||||||
| 			front, body = lines[1:idx], lines[idx+1:] | 			front = contents[frontMatterStart:start] | ||||||
|  | 			body = contents[end+1:] | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(front) == 0 { | 	if len(front) == 0 { | ||||||
| 		return "", errors.New("could not determine metadata") | 		return contents, errors.New("could not determine metadata") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := yaml.Unmarshal([]byte(strings.Join(front, "\n")), out); err != nil { | 	log.Info("%s", string(front)) | ||||||
| 		return "", err | 
 | ||||||
|  | 	if err := yaml.Unmarshal(front, out); err != nil { | ||||||
|  | 		return contents, err | ||||||
| 	} | 	} | ||||||
| 	return strings.Join(body, "\n"), nil | 	return body, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -56,6 +56,38 @@ func TestExtractMetadata(t *testing.T) { | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestExtractMetadataBytes(t *testing.T) { | ||||||
|  | 	t.Run("ValidFrontAndBody", func(t *testing.T) { | ||||||
|  | 		var meta structs.IssueTemplate | ||||||
|  | 		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, bodyTest, body) | ||||||
|  | 		assert.Equal(t, metaTest, meta) | ||||||
|  | 		assert.True(t, validateMetadata(meta)) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("NoFirstSeparator", func(t *testing.T) { | ||||||
|  | 		var meta structs.IssueTemplate | ||||||
|  | 		_, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta) | ||||||
|  | 		assert.Error(t, err) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("NoLastSeparator", func(t *testing.T) { | ||||||
|  | 		var meta structs.IssueTemplate | ||||||
|  | 		_, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta) | ||||||
|  | 		assert.Error(t, err) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("NoBody", func(t *testing.T) { | ||||||
|  | 		var meta structs.IssueTemplate | ||||||
|  | 		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, "", body) | ||||||
|  | 		assert.Equal(t, metaTest, meta) | ||||||
|  | 		assert.True(t, validateMetadata(meta)) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| var ( | var ( | ||||||
| 	sepTest   = "-----" | 	sepTest   = "-----" | ||||||
| 	frontTest = `name: Test | 	frontTest = `name: Test | ||||||
|  |  | ||||||
|  | @ -5,12 +5,11 @@ | ||||||
| package markdown | package markdown | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"github.com/yuin/goldmark/ast" | 	"github.com/yuin/goldmark/ast" | ||||||
| 	east "github.com/yuin/goldmark/extension/ast" | 	"gopkg.in/yaml.v3" | ||||||
| 	"gopkg.in/yaml.v2" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // RenderConfig represents rendering configuration for this file | // RenderConfig represents rendering configuration for this file | ||||||
|  | @ -19,48 +18,46 @@ type RenderConfig struct { | ||||||
| 	Icon     string | 	Icon     string | ||||||
| 	TOC      bool | 	TOC      bool | ||||||
| 	Lang     string | 	Lang     string | ||||||
|  | 	yamlNode *yaml.Node | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ToRenderConfig converts a yaml.MapSlice to a RenderConfig | // UnmarshalYAML implement yaml.v3 UnmarshalYAML | ||||||
| func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) { | func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { | ||||||
| 	if meta == nil { | 	if rc == nil { | ||||||
| 		return | 		rc = &RenderConfig{ | ||||||
|  | 			Meta: "table", | ||||||
|  | 			Icon: "table", | ||||||
|  | 			Lang: "", | ||||||
| 		} | 		} | ||||||
| 	found := false |  | ||||||
| 	var giteaMetaControl yaml.MapItem |  | ||||||
| 	for _, item := range meta { |  | ||||||
| 		strKey, ok := item.Key.(string) |  | ||||||
| 		if !ok { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		strKey = strings.TrimSpace(strings.ToLower(strKey)) |  | ||||||
| 		switch strKey { |  | ||||||
| 		case "gitea": |  | ||||||
| 			giteaMetaControl = item |  | ||||||
| 			found = true |  | ||||||
| 		case "include_toc": |  | ||||||
| 			val, ok := item.Value.(bool) |  | ||||||
| 			if !ok { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			rc.TOC = val |  | ||||||
| 		case "lang": |  | ||||||
| 			val, ok := item.Value.(string) |  | ||||||
| 			if !ok { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			val = strings.TrimSpace(val) |  | ||||||
| 			if len(val) == 0 { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			rc.Lang = val |  | ||||||
| 	} | 	} | ||||||
|  | 	rc.yamlNode = value | ||||||
|  | 
 | ||||||
|  | 	type basicRenderConfig struct { | ||||||
|  | 		Gitea *yaml.Node `yaml:"gitea"` | ||||||
|  | 		TOC   bool       `yaml:"include_toc"` | ||||||
|  | 		Lang  string     `yaml:"lang"` | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if found { | 	var basic basicRenderConfig | ||||||
| 		switch v := giteaMetaControl.Value.(type) { | 
 | ||||||
| 		case string: | 	err := value.Decode(&basic) | ||||||
| 			switch v { | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if basic.Lang != "" { | ||||||
|  | 		rc.Lang = basic.Lang | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	rc.TOC = basic.TOC | ||||||
|  | 	if basic.Gitea == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var control *string | ||||||
|  | 	if err := basic.Gitea.Decode(&control); err == nil && control != nil { | ||||||
|  | 		log.Info("control %v", control) | ||||||
|  | 		switch strings.TrimSpace(strings.ToLower(*control)) { | ||||||
| 		case "none": | 		case "none": | ||||||
| 			rc.Meta = "none" | 			rc.Meta = "none" | ||||||
| 		case "table": | 		case "table": | ||||||
|  | @ -68,20 +65,22 @@ func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) { | ||||||
| 		default: // "details" | 		default: // "details" | ||||||
| 			rc.Meta = "details" | 			rc.Meta = "details" | ||||||
| 		} | 		} | ||||||
| 		case yaml.MapSlice: | 		return nil | ||||||
| 			for _, item := range v { |  | ||||||
| 				strKey, ok := item.Key.(string) |  | ||||||
| 				if !ok { |  | ||||||
| 					continue |  | ||||||
| 	} | 	} | ||||||
| 				strKey = strings.TrimSpace(strings.ToLower(strKey)) | 
 | ||||||
| 				switch strKey { | 	type giteaControl struct { | ||||||
| 				case "meta": | 		Meta string     `yaml:"meta"` | ||||||
| 					val, ok := item.Value.(string) | 		Icon string     `yaml:"details_icon"` | ||||||
| 					if !ok { | 		TOC  *yaml.Node `yaml:"include_toc"` | ||||||
| 						continue | 		Lang string     `yaml:"lang"` | ||||||
| 	} | 	} | ||||||
| 					switch strings.TrimSpace(strings.ToLower(val)) { | 
 | ||||||
|  | 	var controlStruct *giteaControl | ||||||
|  | 	if err := basic.Gitea.Decode(controlStruct); err != nil || controlStruct == nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch strings.TrimSpace(strings.ToLower(controlStruct.Meta)) { | ||||||
| 	case "none": | 	case "none": | ||||||
| 		rc.Meta = "none" | 		rc.Meta = "none" | ||||||
| 	case "table": | 	case "table": | ||||||
|  | @ -89,75 +88,31 @@ func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) { | ||||||
| 	default: // "details" | 	default: // "details" | ||||||
| 		rc.Meta = "details" | 		rc.Meta = "details" | ||||||
| 	} | 	} | ||||||
| 				case "details_icon": | 
 | ||||||
| 					val, ok := item.Value.(string) | 	rc.Icon = strings.TrimSpace(strings.ToLower(controlStruct.Icon)) | ||||||
| 					if !ok { | 
 | ||||||
| 						continue | 	if controlStruct.Lang != "" { | ||||||
| 					} | 		rc.Lang = controlStruct.Lang | ||||||
| 					rc.Icon = strings.TrimSpace(strings.ToLower(val)) |  | ||||||
| 				case "include_toc": |  | ||||||
| 					val, ok := item.Value.(bool) |  | ||||||
| 					if !ok { |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
| 					rc.TOC = val |  | ||||||
| 				case "lang": |  | ||||||
| 					val, ok := item.Value.(string) |  | ||||||
| 					if !ok { |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
| 					val = strings.TrimSpace(val) |  | ||||||
| 					if len(val) == 0 { |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
| 					rc.Lang = val |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	var toc bool | ||||||
|  | 	if err := controlStruct.TOC.Decode(&toc); err == nil { | ||||||
|  | 		rc.TOC = toc | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (rc *RenderConfig) toMetaNode(meta yaml.MapSlice) ast.Node { | func (rc *RenderConfig) toMetaNode() ast.Node { | ||||||
|  | 	if rc.yamlNode == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
| 	switch rc.Meta { | 	switch rc.Meta { | ||||||
| 	case "table": | 	case "table": | ||||||
| 		return metaToTable(meta) | 		return nodeToTable(rc.yamlNode) | ||||||
| 	case "details": | 	case "details": | ||||||
| 		return metaToDetails(meta, rc.Icon) | 		return nodeToDetails(rc.yamlNode, rc.Icon) | ||||||
| 	default: | 	default: | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func metaToTable(meta yaml.MapSlice) ast.Node { |  | ||||||
| 	table := east.NewTable() |  | ||||||
| 	alignments := []east.Alignment{} |  | ||||||
| 	for range meta { |  | ||||||
| 		alignments = append(alignments, east.AlignNone) |  | ||||||
| 	} |  | ||||||
| 	row := east.NewTableRow(alignments) |  | ||||||
| 	for _, item := range meta { |  | ||||||
| 		cell := east.NewTableCell() |  | ||||||
| 		cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Key)))) |  | ||||||
| 		row.AppendChild(row, cell) |  | ||||||
| 	} |  | ||||||
| 	table.AppendChild(table, east.NewTableHeader(row)) |  | ||||||
| 
 |  | ||||||
| 	row = east.NewTableRow(alignments) |  | ||||||
| 	for _, item := range meta { |  | ||||||
| 		cell := east.NewTableCell() |  | ||||||
| 		cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Value)))) |  | ||||||
| 		row.AppendChild(row, cell) |  | ||||||
| 	} |  | ||||||
| 	table.AppendChild(table, row) |  | ||||||
| 	return table |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func metaToDetails(meta yaml.MapSlice, icon string) ast.Node { |  | ||||||
| 	details := NewDetails() |  | ||||||
| 	summary := NewSummary() |  | ||||||
| 	summary.AppendChild(summary, NewIcon(icon)) |  | ||||||
| 	details.AppendChild(details, summary) |  | ||||||
| 	details.AppendChild(details, metaToTable(meta)) |  | ||||||
| 
 |  | ||||||
| 	return details |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										162
									
								
								modules/markup/markdown/renderconfig_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								modules/markup/markdown/renderconfig_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,162 @@ | ||||||
|  | // 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 markdown | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"gopkg.in/yaml.v3" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestRenderConfig_UnmarshalYAML(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name     string | ||||||
|  | 		expected *RenderConfig | ||||||
|  | 		args     string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			"empty", &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			}, "", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"lang", &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "test", | ||||||
|  | 			}, "lang: test", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"metatable", &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			}, "gitea: table", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"metanone", &RenderConfig{ | ||||||
|  | 				Meta: "none", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			}, "gitea: none", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"metadetails", &RenderConfig{ | ||||||
|  | 				Meta: "details", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			}, "gitea: details", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"metawrong", &RenderConfig{ | ||||||
|  | 				Meta: "details", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			}, "gitea: wrong", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"toc", &RenderConfig{ | ||||||
|  | 				TOC:  true, | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			}, "include_toc: true", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"tocfalse", &RenderConfig{ | ||||||
|  | 				TOC:  false, | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			}, "include_toc: false", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"toclang", &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				TOC:  true, | ||||||
|  | 				Lang: "testlang", | ||||||
|  | 			}, ` | ||||||
|  | 	include_toc: true | ||||||
|  | 	lang: testlang | ||||||
|  | `, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"complexlang", &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "testlang", | ||||||
|  | 			}, ` | ||||||
|  | 	gitea: | ||||||
|  | 		lang: testlang | ||||||
|  | `, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"complexlang2", &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "testlang", | ||||||
|  | 			}, ` | ||||||
|  | 	lang: notright | ||||||
|  | 	gitea: | ||||||
|  | 		lang: testlang | ||||||
|  | `, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"complexlang", &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "testlang", | ||||||
|  | 			}, ` | ||||||
|  | 	gitea: | ||||||
|  | 		lang: testlang | ||||||
|  | `, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"complex2", &RenderConfig{ | ||||||
|  | 				Lang: "two", | ||||||
|  | 				Meta: "table", | ||||||
|  | 				TOC:  true, | ||||||
|  | 				Icon: "smiley", | ||||||
|  | 			}, ` | ||||||
|  | 	lang: one | ||||||
|  | 	include_toc: true | ||||||
|  | 	gitea: | ||||||
|  | 		details_icon: smiley | ||||||
|  | 		meta: table | ||||||
|  | 		include_toc: true | ||||||
|  | 		lang: two | ||||||
|  | `, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			got := &RenderConfig{ | ||||||
|  | 				Meta: "table", | ||||||
|  | 				Icon: "table", | ||||||
|  | 				Lang: "", | ||||||
|  | 			} | ||||||
|  | 			if err := yaml.Unmarshal([]byte(tt.args), got); err != nil { | ||||||
|  | 				t.Errorf("RenderConfig.UnmarshalYAML() error = %v", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if got.Meta != tt.expected.Meta { | ||||||
|  | 				t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta) | ||||||
|  | 			} | ||||||
|  | 			if got.Icon != tt.expected.Icon { | ||||||
|  | 				t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon) | ||||||
|  | 			} | ||||||
|  | 			if got.Lang != tt.expected.Lang { | ||||||
|  | 				t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang) | ||||||
|  | 			} | ||||||
|  | 			if got.TOC != tt.expected.TOC { | ||||||
|  | 				t.Errorf("TOC Expected %t Got %t", tt.expected.TOC, got.TOC) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -56,7 +56,7 @@ func createDefaultPolicy() *bluemonday.Policy { | ||||||
| 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre") | 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre") | ||||||
| 
 | 
 | ||||||
| 	// For Chroma markdown plugin | 	// For Chroma markdown plugin | ||||||
| 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code") | 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code") | ||||||
| 
 | 
 | ||||||
| 	// Checkboxes | 	// Checkboxes | ||||||
| 	policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") | 	policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") | ||||||
|  | @ -83,7 +83,7 @@ func createDefaultPolicy() *bluemonday.Policy { | ||||||
| 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") | 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") | ||||||
| 
 | 
 | ||||||
| 	// Allow icons, emojis, chroma syntax and keyword markup on span | 	// Allow icons, emojis, chroma syntax and keyword markup on span | ||||||
| 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") | 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") | ||||||
| 
 | 
 | ||||||
| 	// Allow 'style' attribute on text elements. | 	// Allow 'style' attribute on text elements. | ||||||
| 	policy.AllowAttrs("style").OnElements("span", "p") | 	policy.AllowAttrs("style").OnElements("span", "p") | ||||||
|  |  | ||||||
|  | @ -344,10 +344,12 @@ var ( | ||||||
| 		EnableHardLineBreakInDocuments bool | 		EnableHardLineBreakInDocuments bool | ||||||
| 		CustomURLSchemes               []string `ini:"CUSTOM_URL_SCHEMES"` | 		CustomURLSchemes               []string `ini:"CUSTOM_URL_SCHEMES"` | ||||||
| 		FileExtensions                 []string | 		FileExtensions                 []string | ||||||
|  | 		EnableMath                     bool | ||||||
| 	}{ | 	}{ | ||||||
| 		EnableHardLineBreakInComments:  true, | 		EnableHardLineBreakInComments:  true, | ||||||
| 		EnableHardLineBreakInDocuments: false, | 		EnableHardLineBreakInDocuments: false, | ||||||
| 		FileExtensions:                 strings.Split(".md,.markdown,.mdown,.mkd", ","), | 		FileExtensions:                 strings.Split(".md,.markdown,.mdown,.mkd", ","), | ||||||
|  | 		EnableMath:                     true, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Admin settings | 	// Admin settings | ||||||
|  |  | ||||||
|  | @ -3092,6 +3092,7 @@ container.details.platform = Platform | ||||||
| container.details.repository_site = Repository Site | container.details.repository_site = Repository Site | ||||||
| container.details.documentation_site = Documentation Site | container.details.documentation_site = Documentation Site | ||||||
| container.pull = Pull the image from the command line: | container.pull = Pull the image from the command line: | ||||||
|  | container.digest = Digest: | ||||||
| container.documentation = For more information on the Container registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/container/">the documentation</a>. | container.documentation = For more information on the Container registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/container/">the documentation</a>. | ||||||
| container.multi_arch = OS / Arch | container.multi_arch = OS / Arch | ||||||
| container.layers = Image Layers | container.layers = Image Layers | ||||||
|  |  | ||||||
							
								
								
									
										39
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										39
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -20,6 +20,7 @@ | ||||||
|         "font-awesome": "4.7.0", |         "font-awesome": "4.7.0", | ||||||
|         "jquery": "3.6.1", |         "jquery": "3.6.1", | ||||||
|         "jquery.are-you-sure": "1.9.0", |         "jquery.are-you-sure": "1.9.0", | ||||||
|  |         "katex": "0.16.2", | ||||||
|         "less": "4.1.3", |         "less": "4.1.3", | ||||||
|         "less-loader": "11.0.0", |         "less-loader": "11.0.0", | ||||||
|         "license-checker-webpack-plugin": "0.2.1", |         "license-checker-webpack-plugin": "0.2.1", | ||||||
|  | @ -7750,6 +7751,29 @@ | ||||||
|       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", |       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", | ||||||
|       "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" |       "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/katex": { | ||||||
|  |       "version": "0.16.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz", | ||||||
|  |       "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==", | ||||||
|  |       "funding": [ | ||||||
|  |         "https://opencollective.com/katex", | ||||||
|  |         "https://github.com/sponsors/katex" | ||||||
|  |       ], | ||||||
|  |       "dependencies": { | ||||||
|  |         "commander": "^8.0.0" | ||||||
|  |       }, | ||||||
|  |       "bin": { | ||||||
|  |         "katex": "cli.js" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/katex/node_modules/commander": { | ||||||
|  |       "version": "8.3.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", | ||||||
|  |       "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 12" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/khroma": { |     "node_modules/khroma": { | ||||||
|       "version": "2.0.0", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", |       "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", | ||||||
|  | @ -17717,6 +17741,21 @@ | ||||||
|       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", |       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", | ||||||
|       "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" |       "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" | ||||||
|     }, |     }, | ||||||
|  |     "katex": { | ||||||
|  |       "version": "0.16.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz", | ||||||
|  |       "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==", | ||||||
|  |       "requires": { | ||||||
|  |         "commander": "^8.0.0" | ||||||
|  |       }, | ||||||
|  |       "dependencies": { | ||||||
|  |         "commander": { | ||||||
|  |           "version": "8.3.0", | ||||||
|  |           "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", | ||||||
|  |           "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "khroma": { |     "khroma": { | ||||||
|       "version": "2.0.0", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", |       "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ | ||||||
|     "font-awesome": "4.7.0", |     "font-awesome": "4.7.0", | ||||||
|     "jquery": "3.6.1", |     "jquery": "3.6.1", | ||||||
|     "jquery.are-you-sure": "1.9.0", |     "jquery.are-you-sure": "1.9.0", | ||||||
|  |     "katex": "0.16.2", | ||||||
|     "less": "4.1.3", |     "less": "4.1.3", | ||||||
|     "less-loader": "11.0.0", |     "less-loader": "11.0.0", | ||||||
|     "license-checker-webpack-plugin": "0.2.1", |     "license-checker-webpack-plugin": "0.2.1", | ||||||
|  |  | ||||||
|  | @ -427,5 +427,5 @@ func CreateBranch(ctx *context.Context) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName)) | 	ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName)) | ||||||
| 	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName)) | 	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName) + "/" + util.PathEscapeSegments(form.CurrentPath)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -375,7 +375,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | ||||||
| 	ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) | 	ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) | ||||||
| 
 | 
 | ||||||
| 	if ctx.Repo.TreePath == ".editorconfig" { | 	if ctx.Repo.TreePath == ".editorconfig" { | ||||||
| 		_, editorconfigErr := ctx.Repo.GetEditorconfig() | 		_, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) | ||||||
| 		ctx.Data["FileError"] = editorconfigErr | 		ctx.Data["FileError"] = editorconfigErr | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ import ( | ||||||
| // NewBranchForm form for creating a new branch | // NewBranchForm form for creating a new branch | ||||||
| type NewBranchForm struct { | type NewBranchForm struct { | ||||||
| 	NewBranchName string `binding:"Required;MaxSize(100);GitRefName"` | 	NewBranchName string `binding:"Required;MaxSize(100);GitRefName"` | ||||||
|  | 	CurrentPath   string | ||||||
| 	CreateTag     bool | 	CreateTag     bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -151,7 +151,7 @@ | ||||||
| 
 | 
 | ||||||
| 				<div class="field"> | 				<div class="field"> | ||||||
| 					<button class="ui green button">{{.locale.Tr "admin.users.update_profile"}}</button> | 					<button class="ui green button">{{.locale.Tr "admin.users.update_profile"}}</button> | ||||||
| 					<div class="ui red button show-modal" data-modal="#delete-user-modal" data-url="{{$.Link}}/delete" data-id="{{.User.ID}}">{{.locale.Tr "admin.users.delete_account"}}</div> | 					<div class="ui red button show-modal" data-modal="#delete-user-modal">{{.locale.Tr "admin.users.delete_account"}}</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			</form> | 			</form> | ||||||
| 		</div> | 		</div> | ||||||
|  | @ -206,7 +206,6 @@ | ||||||
| 	</div> | 	</div> | ||||||
| 	<form class="ui form" method="POST" action="{{.Link}}/delete"> | 	<form class="ui form" method="POST" action="{{.Link}}/delete"> | ||||||
| 		{{$.CsrfTokenHtml}} | 		{{$.CsrfTokenHtml}} | ||||||
| 		<input type="hidden" name="id"> |  | ||||||
| 		<div class="field"> | 		<div class="field"> | ||||||
| 			<div class="ui checkbox"> | 			<div class="ui checkbox"> | ||||||
| 				<label for="purge">{{.locale.Tr "admin.users.purge"}}</label> | 				<label for="purge">{{.locale.Tr "admin.users.purge"}}</label> | ||||||
|  | @ -214,7 +213,16 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 			<p class="help">{{.locale.Tr "admin.users.purge_help"}}</p> | 			<p class="help">{{.locale.Tr "admin.users.purge_help"}}</p> | ||||||
| 		</div> | 		</div> | ||||||
| 		{{template "base/delete_modal_actions" .}} | 		<div class="actions"> | ||||||
|  | 			<div class="ui red basic inverted cancel button"> | ||||||
|  | 				{{svg "octicon-x"}} | ||||||
|  | 				{{.locale.Tr "modal.no"}} | ||||||
|  | 			</div> | ||||||
|  | 			<button class="ui green basic inverted ok button"> | ||||||
|  | 				{{svg "octicon-check"}} | ||||||
|  | 				{{.locale.Tr "modal.yes"}} | ||||||
|  | 			</button> | ||||||
|  | 		</div> | ||||||
| 	</form> | 	</form> | ||||||
| </div> | </div> | ||||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
| 				{{template "org/team/navbar" .}} | 				{{template "org/team/navbar" .}} | ||||||
| 				{{if .IsOrganizationOwner}} | 				{{if .IsOrganizationOwner}} | ||||||
| 					<div class="ui attached segment"> | 					<div class="ui attached segment"> | ||||||
| 						<form class="ui form" id="add-member-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post"> | 						<form class="ui form ignore-dirty" id="add-member-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post"> | ||||||
| 							{{.CsrfTokenHtml}} | 							{{.CsrfTokenHtml}} | ||||||
| 							<input type="hidden" name="uid" value="{{.SignedUser.ID}}"> | 							<input type="hidden" name="uid" value="{{.SignedUser.ID}}"> | ||||||
| 							<div class="inline field ui left"> | 							<div class="inline field ui left"> | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
| 				{{if $canAddRemove}} | 				{{if $canAddRemove}} | ||||||
| 					<div class="ui attached segment" id="repo-top-segment"> | 					<div class="ui attached segment" id="repo-top-segment"> | ||||||
| 						<div class="inline ui field left"> | 						<div class="inline ui field left"> | ||||||
| 							<form class="ui form" id="add-repo-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post"> | 							<form class="ui form ignore-dirty" id="add-repo-form" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post"> | ||||||
| 								{{.CsrfTokenHtml}} | 								{{.CsrfTokenHtml}} | ||||||
| 								<div class="inline field ui left"> | 								<div class="inline field ui left"> | ||||||
| 									<div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search"> | 									<div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search"> | ||||||
|  |  | ||||||
|  | @ -14,6 +14,10 @@ | ||||||
| 					<div class="markup"><pre class="code-block"><code>docker pull {{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}</code></pre></div> | 					<div class="markup"><pre class="code-block"><code>docker pull {{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}</code></pre></div> | ||||||
| 				{{end}} | 				{{end}} | ||||||
| 			</div> | 			</div> | ||||||
|  | 			<div class="field"> | ||||||
|  | 				<label>{{svg "octicon-code"}} {{.locale.Tr "packages.container.digest"}}</label> | ||||||
|  | 				<div class="markup"><pre class="code-block"><code>{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}</code></pre></div> | ||||||
|  | 			</div> | ||||||
| 			<div class="field"> | 			<div class="field"> | ||||||
| 				<label>{{.locale.Tr "packages.container.documentation" | Safe}}</label> | 				<label>{{.locale.Tr "packages.container.documentation" | Safe}}</label> | ||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
|  | @ -94,6 +94,9 @@ | ||||||
| 						{{.root.CsrfTokenHtml}} | 						{{.root.CsrfTokenHtml}} | ||||||
| 						<input type="hidden" name="new_branch_name" v-model="searchTerm"> | 						<input type="hidden" name="new_branch_name" v-model="searchTerm"> | ||||||
| 						<input type="hidden" name="create_tag" v-model="createTag"> | 						<input type="hidden" name="create_tag" v-model="createTag"> | ||||||
|  | 						{{if $.root.TreePath}} | ||||||
|  | 							<input type="hidden" name="current_path" value="{{.root.TreePath}}"> | ||||||
|  | 						{{end}} | ||||||
| 					</form> | 					</form> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
|  | @ -1,10 +1,12 @@ | ||||||
| import {renderMermaid} from './mermaid.js'; | import {renderMermaid} from './mermaid.js'; | ||||||
|  | import {renderMath} from './math.js'; | ||||||
| import {renderCodeCopy} from './codecopy.js'; | import {renderCodeCopy} from './codecopy.js'; | ||||||
| import {initMarkupTasklist} from './tasklist.js'; | import {initMarkupTasklist} from './tasklist.js'; | ||||||
| 
 | 
 | ||||||
| // code that runs for all markup content
 | // code that runs for all markup content
 | ||||||
| export function initMarkupContent() { | export function initMarkupContent() { | ||||||
|   renderMermaid(); |   renderMermaid(); | ||||||
|  |   renderMath(); | ||||||
|   renderCodeCopy(); |   renderCodeCopy(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										37
									
								
								web_src/js/markup/math.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web_src/js/markup/math.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | function displayError(el, err) { | ||||||
|  |   const target = targetElement(el); | ||||||
|  |   target.remove('is-loading'); | ||||||
|  |   const errorNode = document.createElement('div'); | ||||||
|  |   errorNode.setAttribute('class', 'ui message error markup-block-error mono'); | ||||||
|  |   errorNode.textContent = err.str || err.message || String(err); | ||||||
|  |   target.before(errorNode); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function targetElement(el) { | ||||||
|  |   // The target element is either the current element if it has the `is-loading` class or the pre that contains it
 | ||||||
|  |   return el.classList.contains('is-loading') ? el : el.closest('pre'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function renderMath() { | ||||||
|  |   const els = document.querySelectorAll('.markup code.language-math'); | ||||||
|  |   if (!els.length) return; | ||||||
|  | 
 | ||||||
|  |   const [{default: katex}] = await Promise.all([ | ||||||
|  |     import(/* webpackChunkName: "katex" */'katex'), | ||||||
|  |     import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), | ||||||
|  |   ]); | ||||||
|  | 
 | ||||||
|  |   for (const el of els) { | ||||||
|  |     const source = el.textContent; | ||||||
|  |     const options = {display: el.classList.contains('display')}; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       const markup = katex.renderToString(source, options); | ||||||
|  |       const tempEl = document.createElement(options.display ? 'p' : 'span'); | ||||||
|  |       tempEl.innerHTML = markup; | ||||||
|  |       targetElement(el).replaceWith(tempEl); | ||||||
|  |     } catch (error) { | ||||||
|  |       displayError(el, error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -33,6 +33,13 @@ | ||||||
|   height: var(--height-loading); |   height: var(--height-loading); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | code.language-math.is-loading::after { | ||||||
|  |   padding: 0; | ||||||
|  |   border-width: 2px; | ||||||
|  |   width: 1.25rem; | ||||||
|  |   height: 1.25rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @keyframes fadein { | @keyframes fadein { | ||||||
|   0% { |   0% { | ||||||
|     opacity: 0; |     opacity: 0; | ||||||
|  |  | ||||||
|  | @ -37,6 +37,10 @@ const filterCssImport = (url, ...args) => { | ||||||
|     if (/(eot|ttf|otf|woff|svg)$/.test(importedFile)) return false; |     if (/(eot|ttf|otf|woff|svg)$/.test(importedFile)) return false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   if (cssFile.includes('katex') && /(ttf|woff)$/.test(importedFile)) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   if (cssFile.includes('font-awesome') && /(eot|ttf|otf|woff|svg)$/.test(importedFile)) { |   if (cssFile.includes('font-awesome') && /(eot|ttf|otf|woff|svg)$/.test(importedFile)) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Anthony Wang
						Anthony Wang