Merge remote-tracking branch 'upstream/main'
commit
47229ea208
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -31,101 +31,101 @@ _表格中的符号含义:_
|
||||||
|
|
||||||
#### 主要特性
|
#### 主要特性
|
||||||
|
|
||||||
| 特性 | 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) | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ |
|
||||||
| 支持多种数据库 | ✓ | ✓ | ✘ | ⁄ | ⁄ | ✓ | ✓ |
|
| 支持多种数据库 | ✓ | ✓ | ✘ | ⁄ | ⁄ | ✓ | ✓ |
|
||||||
| 支持多种操作系统 | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ |
|
| 支持多种操作系统 | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ |
|
||||||
| 升级简便 | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ |
|
| 升级简便 | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ |
|
||||||
| 支持 Markdown | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| 支持 Markdown | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| 支持 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 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| 支持多个 LDAP / AD 服务 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ |
|
| 支持多个 LDAP / AD 服务 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ |
|
||||||
| LDAP 用户同步 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| LDAP 用户同步 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| SAML 2.0 service provider | [✘](https://github.com/go-gitea/gitea/issues/5512) | [✘](https://github.com/gogs/gogs/issues/1221) | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| SAML 2.0 service provider | [✘](https://github.com/go-gitea/gitea/issues/5512) | [✘](https://github.com/gogs/gogs/issues/1221) | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
| 支持 OpenId 连接 | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ |
|
| 支持 OpenId 连接 | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ |
|
||||||
| 集成 OAuth 2.0(外部授权) | ✓ | ✘ | ⁄ | ✓ | ✓ | ? | ✓ |
|
| 集成 OAuth 2.0(外部授权) | ✓ | ✘ | ⁄ | ✓ | ✓ | ? | ✓ |
|
||||||
| 作为 OAuth 2.0 provider | [✓](https://github.com/go-gitea/gitea/pull/5378) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| 作为 OAuth 2.0 provider | [✓](https://github.com/go-gitea/gitea/pull/5378) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
| 二次验证 (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| 二次验证 (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
| 集成 Mattermost/Slack | ✓ | ✓ | ⁄ | ✓ | ✓ | ⁄ | ✓ |
|
| 集成 Mattermost/Slack | ✓ | ✓ | ⁄ | ✓ | ✓ | ⁄ | ✓ |
|
||||||
| 集成 Discord | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| 集成 Discord | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| 集成 Microsoft Teams | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| 集成 Microsoft Teams | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
| 显示外部 CI/CD 的状态 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| 显示外部 CI/CD 的状态 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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{},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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,
|
||||||
if !isYAMLSeparator(line) {
|
// and returns the frontmatter metadata separated from the markdown content
|
||||||
return "", errors.New("frontmatter must start with a separator line")
|
func ExtractMetadataBytes(contents []byte, out interface{}) ([]byte, error) {
|
||||||
}
|
var front, body []byte
|
||||||
continue
|
|
||||||
|
start, end := 0, len(contents)
|
||||||
|
idx := bytes.IndexByte(contents[start:], '\n')
|
||||||
|
if idx >= 0 {
|
||||||
|
end = start + idx
|
||||||
|
}
|
||||||
|
line := contents[start:end]
|
||||||
|
|
||||||
|
if !isYAMLSeparator(line) {
|
||||||
|
return contents, errors.New("frontmatter must start with a separator line")
|
||||||
|
}
|
||||||
|
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,159 +5,114 @@
|
||||||
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
|
||||||
type RenderConfig struct {
|
type RenderConfig struct {
|
||||||
Meta string
|
Meta string
|
||||||
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
|
rc.yamlNode = value
|
||||||
var giteaMetaControl yaml.MapItem
|
|
||||||
for _, item := range meta {
|
type basicRenderConfig struct {
|
||||||
strKey, ok := item.Key.(string)
|
Gitea *yaml.Node `yaml:"gitea"`
|
||||||
if !ok {
|
TOC bool `yaml:"include_toc"`
|
||||||
continue
|
Lang string `yaml:"lang"`
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if found {
|
var basic basicRenderConfig
|
||||||
switch v := giteaMetaControl.Value.(type) {
|
|
||||||
case string:
|
err := value.Decode(&basic)
|
||||||
switch v {
|
if err != nil {
|
||||||
case "none":
|
return err
|
||||||
rc.Meta = "none"
|
|
||||||
case "table":
|
|
||||||
rc.Meta = "table"
|
|
||||||
default: // "details"
|
|
||||||
rc.Meta = "details"
|
|
||||||
}
|
|
||||||
case yaml.MapSlice:
|
|
||||||
for _, item := range v {
|
|
||||||
strKey, ok := item.Key.(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
strKey = strings.TrimSpace(strings.ToLower(strKey))
|
|
||||||
switch strKey {
|
|
||||||
case "meta":
|
|
||||||
val, ok := item.Value.(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch strings.TrimSpace(strings.ToLower(val)) {
|
|
||||||
case "none":
|
|
||||||
rc.Meta = "none"
|
|
||||||
case "table":
|
|
||||||
rc.Meta = "table"
|
|
||||||
default: // "details"
|
|
||||||
rc.Meta = "details"
|
|
||||||
}
|
|
||||||
case "details_icon":
|
|
||||||
val, ok := item.Value.(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
rc.Meta = "none"
|
||||||
|
case "table":
|
||||||
|
rc.Meta = "table"
|
||||||
|
default: // "details"
|
||||||
|
rc.Meta = "details"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type giteaControl struct {
|
||||||
|
Meta string `yaml:"meta"`
|
||||||
|
Icon string `yaml:"details_icon"`
|
||||||
|
TOC *yaml.Node `yaml:"include_toc"`
|
||||||
|
Lang string `yaml:"lang"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlStruct *giteaControl
|
||||||
|
if err := basic.Gitea.Decode(controlStruct); err != nil || controlStruct == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.TrimSpace(strings.ToLower(controlStruct.Meta)) {
|
||||||
|
case "none":
|
||||||
|
rc.Meta = "none"
|
||||||
|
case "table":
|
||||||
|
rc.Meta = "table"
|
||||||
|
default: // "details"
|
||||||
|
rc.Meta = "details"
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.Icon = strings.TrimSpace(strings.ToLower(controlStruct.Icon))
|
||||||
|
|
||||||
|
if controlStruct.Lang != "" {
|
||||||
|
rc.Lang = controlStruct.Lang
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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…
Reference in New Issue