From 2854031d870434f4a9f7e5131846eb6a7209ca59 Mon Sep 17 00:00:00 2001 From: John Olheiser Date: Mon, 12 Sep 2022 15:52:17 -0500 Subject: [PATCH 1/9] Use form for admin purge user (#21070) Fixes #20998 The basic modal actions were set up for basic confirmation-style modals, however this modal also has a special form input, which instead requires a form in the modal itself. The basic modal actions are indirectly controlled by JS and are simple `
` elements, whereas this requires a ` -
{{.locale.Tr "admin.users.delete_account"}}
+
{{.locale.Tr "admin.users.delete_account"}}
@@ -206,7 +206,6 @@
{{$.CsrfTokenHtml}} -
@@ -214,7 +213,16 @@

{{.locale.Tr "admin.users.purge_help"}}

- {{template "base/delete_modal_actions" .}} +
+
+ {{svg "octicon-x"}} + {{.locale.Tr "modal.no"}} +
+ +
{{template "base/footer" .}} From eaa561145ac276b9ba8b19e1abd86e97f1a05225 Mon Sep 17 00:00:00 2001 From: ya Date: Tue, 13 Sep 2022 12:16:32 +0800 Subject: [PATCH 2/9] Update docs comparison.zh-cn.md (#21035) - Update Chinese translation from comparison.en-us.md Co-authored-by: Lunny Xiao Co-authored-by: 6543 <6543@obermui.de> --- docs/content/doc/features/comparison.zh-cn.md | 174 +++++++++--------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/docs/content/doc/features/comparison.zh-cn.md b/docs/content/doc/features/comparison.zh-cn.md index f0eac22a26..aaf8eb4abc 100644 --- a/docs/content/doc/features/comparison.zh-cn.md +++ b/docs/content/doc/features/comparison.zh-cn.md @@ -31,101 +31,101 @@ _表格中的符号含义:_ #### 主要特性 -| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -|-----------------------|-------|------|-----------|-----------|-----------|-----------|--------------| -| 开源免费 | ✓ | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | -| 低资源开销 (RAM/CPU) | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | -| 支持多种数据库 | ✓ | ✓ | ✘ | ⁄ | ⁄ | ✓ | ✓ | -| 支持多种操作系统 | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | -| 升级简便 | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ | -| 支持 Markdown | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 支持 Orgmode | ✓ | ✘ | ✓ | ✘ | ✘ | ✘ | ? | -| 支持 CSV | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | ? | -| 支持第三方渲染工具 | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | ? | -| Git 驱动的静态 pages | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Git 驱动的集成化 wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | -| 部署令牌 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 仓库写权限令牌 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ | -| 内置容器 Registry | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| 外部 Git 镜像 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | -| WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | -| 内置 CI/CD | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| 子组织:组织内的组织 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | +| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| --------------------- | -------------------------------------------------- | ---- | --------- | --------- | --------- | -------------- | ------------ | +| 开源免费 | ✓ | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | +| 低资源开销 (RAM/CPU) | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | +| 支持多种数据库 | ✓ | ✓ | ✘ | ⁄ | ⁄ | ✓ | ✓ | +| 支持多种操作系统 | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | +| 升级简便 | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ | +| 支持 Markdown | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 支持 Orgmode | ✓ | ✘ | ✓ | ✘ | ✘ | ✘ | ? | +| 支持 CSV | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | ? | +| 支持第三方渲染工具 | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | ? | +| Git 驱动的静态 pages | [✘](https://github.com/go-gitea/gitea/issues/302) | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Git 驱动的集成化 wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (cloud only) | ✘ | +| 部署令牌 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 仓库写权限令牌 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 内置容器 Registry | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 外部 Git 镜像 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | +| WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | +| 内置 CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 子组织:组织内的组织 | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | #### 代码管理 -| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -|------------------------------------------|-------|------|-----------|-----------|-----------|-----------|--------------| -| 仓库主题描述 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| 仓库内代码搜索 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 全局代码搜索 | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ | -| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | ⁄ | ✓ | -| 组织里程碑 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| 细粒度用户角色 (例如 Code, Issues, Wiki) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| 提交人的身份验证 | ⁄ | ✘ | ? | ✓ | ✓ | ✓ | ✘ | -| GPG 签名的提交 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| SSH 签名的提交 | ✓ | ✘ | ✘ | ✘ | ✘ | ? | ? | -| 拒绝未用通过验证的提交 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ | -| 仓库活跃度页面 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 分支管理 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 建立新分支 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| 在线代码编辑 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 提交的统计图表 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 模板仓库 | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✘ | +| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ---------------------------------------- | ------------------------------------------------ | ---- | --------- | --------- | --------- | --------- | ------------ | +| 仓库主题描述 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 仓库内代码搜索 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 全局代码搜索 | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ | +| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 组织里程碑 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| 细粒度用户角色 (例如 Code, Issues, Wiki) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| 提交人的身份验证 | ⁄ | ✘ | ? | ✓ | ✓ | ✓ | ✘ | +| GPG 签名的提交 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 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 | -|----------------------|-------|------|-----------|-----------|-----------|-----------|--------------| -| 跟踪 Issue | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Issue 模板 | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| 标签 | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| 跟踪时间 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Issue 可有多个负责人 | ✓ | ✘ | ✓ | ✘ | ✓ | ✘ | ✘ | -| 关联的 issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ | -| 私密 issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| 评论反馈 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| 锁定讨论 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Issue 批量处理 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Issue 看板 | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| 从 issues 创建分支 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| Issue 搜索 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| 全局 Issue 搜索 | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Issue 依赖 | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ | -| 通过 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) | ✘ | ✘ | ✘ | ✓ | ✘ | ✘ | +| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ------------------- | -------------------------------------------------- | --------------------------------------------- | --------- | ----------------------------------------------------------------------- | --------- | -------------- | ------------ | +| 工单跟踪 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (cloud only) | ✘ | +| 工单模板 | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 标签 | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 时间跟踪 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 支持多个负责人 | ✓ | ✘ | ✓ | ✘ | ✓ | ✘ | ✘ | +| 关联的工单 | ✘ | ✘ | ⁄ | [✓](https://docs.gitlab.com/ce/user/project/issues/related_issues.html) | ✓ | ✘ | ✘ | +| 私密工单 | [✘](https://github.com/go-gitea/gitea/issues/3217) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| 评论反馈 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 锁定讨论 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 工单批处理 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 工单看板 | [✓](https://github.com/go-gitea/gitea/pull/8346) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| 从工单创建分支 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| 工单搜索 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| 工单全局搜索 | [✘](https://github.com/go-gitea/gitea/issues/2434) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| 工单依赖关系 | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ | +| 通过 Email 创建工单 | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘ | ✘ | ✓ | ✓ | ✘ | +| 服务台 | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘ | ✘ | [✓](https://gitlab.com/groups/gitlab-org/-/epics/3103) | ✓ | ✘ | ✘ | #### Pull/Merge requests -| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -|--------------------------------------|-------|------|-----------|-----------|-----------|-----------|--------------| -| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Squash merging | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ | -| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ | -| 评论 Pull/Merge request 中的某行代码 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 指定 Pull/Merge request 的审核人 | ✓ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ | -| 解决 Merge 冲突 | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| 限制某些用户的 push 和 merge 权限 | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ | -| 回退某些 commits 或 merge request | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Pull/Merge requests 模板 | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| 查看 Cherry-picking 的更改 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| 下载 Patch | ✓ | ✘ | ✓ | ✓ | ✓ | / | ✘ | +| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ------------------------------------ | -------------------------------------------------- | ---- | --------- | --------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------ | ------------ | +| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Squash merging | ✓ | ✘ | ✓ | [✓](https://docs.gitlab.com/ce/user/project/merge_requests/squash_and_merge.html) | ✓ | ✓ | ✓ | +| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ | +| 评论 Pull/Merge request 中的某行代码 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 指定 Pull/Merge request 的审核人 | ✓ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ | +| 解决 Merge 冲突 | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| 限制某些用户的 push 和 merge 权限 | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ | +| 回退某些 commits 或 merge request | [✓](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Pull/Merge requests 模板 | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 查看 Cherry-picking 的更改 | [✓](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| 下载 Patch | ✓ | ✘ | ✓ | ✓ | ✓ | [/](https://jira.atlassian.com/plugins/servlet/mobile#issue/BCLOUD-8323) | ✘ | #### 第三方集成 -| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -|----------------------------|-------|------|-----------|-----------|-----------|-----------|--------------| -| 支持 Webhook | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 自定义 Git 钩子 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 集成 AD / LDAP | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| 支持多个 LDAP / AD 服务 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | -| LDAP 用户同步 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| SAML 2.0 service provider | [✘](https://github.com/go-gitea/gitea/issues/5512) | [✘](https://github.com/gogs/gogs/issues/1221) | ✓ | ✓ | ✓ | ✓ | ✘ | -| 支持 OpenId 连接 | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ | -| 集成 OAuth 2.0(外部授权) | ✓ | ✘ | ⁄ | ✓ | ✓ | ? | ✓ | -| 作为 OAuth 2.0 provider | [✓](https://github.com/go-gitea/gitea/pull/5378) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| 二次验证 (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | -| 集成 Mattermost/Slack | ✓ | ✓ | ⁄ | ✓ | ✓ | ⁄ | ✓ | -| 集成 Discord | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| 集成 Microsoft Teams | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| 显示外部 CI/CD 的状态 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 特性 | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| -------------------------- | -------------------------------------------------- | --------------------------------------------- | --------- | --------- | --------- | --------- | ------------ | +| 支持 Webhook | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 自定义 Git 钩子 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 集成 AD / LDAP | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| 支持多个 LDAP / AD 服务 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | +| LDAP 用户同步 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| SAML 2.0 service provider | [✘](https://github.com/go-gitea/gitea/issues/5512) | [✘](https://github.com/gogs/gogs/issues/1221) | ✓ | ✓ | ✓ | ✓ | ✘ | +| 支持 OpenId 连接 | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ | +| 集成 OAuth 2.0(外部授权) | ✓ | ✘ | ⁄ | ✓ | ✓ | ? | ✓ | +| 作为 OAuth 2.0 provider | [✓](https://github.com/go-gitea/gitea/pull/5378) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| 二次验证 (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | +| 集成 Mattermost/Slack | ✓ | ✓ | ⁄ | ✓ | ✓ | ⁄ | ✓ | +| 集成 Discord | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| 集成 Microsoft Teams | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| 显示外部 CI/CD 的状态 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | From 88c2e2436001c868c2583950b5f9eb0516424b7d Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 13 Sep 2022 17:33:37 +0100 Subject: [PATCH 3/9] Add KaTeX rendering to Markdown. (#20571) This PR adds mathematical rendering with KaTeX. The first step is to add a Goldmark extension that detects the latex (and tex) mathematics delimiters. The second step to make this extension only run if math support is enabled. The second step is to then add KaTeX CSS and JS to the head which will load after the dom is rendered. Fix #3445 Signed-off-by: Andrew Thornton Signed-off-by: Andrew Thornton Co-authored-by: silverwind Co-authored-by: Lunny Xiao --- custom/conf/app.example.ini | 3 + .../doc/advanced/config-cheat-sheet.en-us.md | 1 + .../doc/advanced/external-renderers.en-us.md | 3 +- docs/content/doc/features/comparison.en-us.md | 2 + docs/content/page/index.en-us.md | 3 +- go.mod | 2 +- modules/markup/markdown/convertyaml.go | 84 +++++++ modules/markup/markdown/goldmark.go | 15 +- modules/markup/markdown/markdown.go | 20 +- modules/markup/markdown/math/block_node.go | 42 ++++ modules/markup/markdown/math/block_parser.go | 123 ++++++++++ .../markup/markdown/math/block_renderer.go | 43 ++++ modules/markup/markdown/math/inline_node.go | 49 ++++ modules/markup/markdown/math/inline_parser.go | 99 ++++++++ .../markup/markdown/math/inline_renderer.go | 47 ++++ modules/markup/markdown/math/math.go | 108 +++++++++ modules/markup/markdown/meta.go | 96 ++++++-- modules/markup/markdown/meta_test.go | 32 +++ modules/markup/markdown/renderconfig.go | 223 +++++++----------- modules/markup/markdown/renderconfig_test.go | 162 +++++++++++++ modules/markup/sanitizer.go | 4 +- modules/setting/setting.go | 2 + package-lock.json | 39 +++ package.json | 1 + web_src/js/markup/content.js | 2 + web_src/js/markup/math.js | 37 +++ web_src/less/animations.less | 7 + webpack.config.js | 4 + 28 files changed, 1079 insertions(+), 174 deletions(-) create mode 100644 modules/markup/markdown/convertyaml.go create mode 100644 modules/markup/markdown/math/block_node.go create mode 100644 modules/markup/markdown/math/block_parser.go create mode 100644 modules/markup/markdown/math/block_renderer.go create mode 100644 modules/markup/markdown/math/inline_node.go create mode 100644 modules/markup/markdown/math/inline_parser.go create mode 100644 modules/markup/markdown/math/inline_renderer.go create mode 100644 modules/markup/markdown/math/math.go create mode 100644 modules/markup/markdown/renderconfig_test.go create mode 100644 web_src/js/markup/math.js diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 1561a53da0..6e1eca7a62 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1262,6 +1262,9 @@ ROUTER = console ;; 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 ;FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd +;; +;; Enables math inline and block detection +;ENABLE_MATH = true ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 05cedeb63d..459e42ac24 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -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 URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are always displayed +- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]`, `$...$` and `$$...$$` blocks as math blocks. ## Server (`server`) diff --git a/docs/content/doc/advanced/external-renderers.en-us.md b/docs/content/doc/advanced/external-renderers.en-us.md index 4e5e72554d..f40c23dd84 100644 --- a/docs/content/doc/advanced/external-renderers.en-us.md +++ b/docs/content/doc/advanced/external-renderers.en-us.md @@ -74,12 +74,13 @@ RENDER_COMMAND = "timeout 30s pandoc +RTS -M512M -RTS -f rst" 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 [markup.sanitizer.TeX] ; Pandoc renders TeX segments as s with the "math" class, optionally ; with "inline" or "display" classes depending on context. +; - note this is different from the built-in math support in our markdown parser which uses ELEMENT = span ALLOW_ATTR = class REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+ diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 3c2a3f9162..89d92b2565 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -53,6 +53,8 @@ _Symbols used in table:_ | WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | | Built-in CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | Subgroups: groups within groups | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | +| Mermaid diagrams in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Math syntax in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ## Code management diff --git a/docs/content/page/index.en-us.md b/docs/content/page/index.en-us.md index 8e2e36223a..c3ee996f0b 100644 --- a/docs/content/page/index.en-us.md +++ b/docs/content/page/index.en-us.md @@ -131,7 +131,8 @@ You can try it out using [the online demo](https://try.gitea.io/). - Environment variables - Command line options - 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 - Notifications - Registration confirmation diff --git a/go.mod b/go.mod index 0e93ea443f..8976900d92 100644 --- a/go.mod +++ b/go.mod @@ -103,6 +103,7 @@ require ( gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 mvdan.cc/xurls/v2 v2.4.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.11 @@ -290,7 +291,6 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // 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 ) diff --git a/modules/markup/markdown/convertyaml.go b/modules/markup/markdown/convertyaml.go new file mode 100644 index 0000000000..3f5ebec908 --- /dev/null +++ b/modules/markup/markdown/convertyaml.go @@ -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 +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 1750128dec..24f1ab7a01 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" - meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" "github.com/yuin/goldmark/parser" @@ -32,20 +31,12 @@ type ASTTransformer struct{} // Transform transforms the given AST tree. func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { - metaData := meta.GetItems(pc) firstChild := node.FirstChild() createTOC := false ctx := pc.Get(renderContextKey).(*markup.RenderContext) - rc := &RenderConfig{ - Meta: "table", - Icon: "table", - Lang: "", - } - - if metaData != nil { - rc.ToRenderConfig(metaData) - - metaNode := rc.toMetaNode(metaData) + rc := pc.Get(renderConfigKey).(*RenderConfig) + if rc.yamlNode != nil { + metaNode := rc.toMetaNode() if metaNode != nil { node.InsertBefore(node, firstChild, metaNode) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 4ce85dfc31..c0e72fd6ce 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" + "code.gitea.io/gitea/modules/markup/markdown/math" "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" @@ -38,6 +39,7 @@ var ( isWikiKey = parser.NewContextKey() renderMetasKey = parser.NewContextKey() renderContextKey = parser.NewContextKey() + renderConfigKey = parser.NewContextKey() ) type limitWriter struct { @@ -98,7 +100,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) languageStr := string(language) preClasses := []string{"code-block"} - if languageStr == "mermaid" { + if languageStr == "mermaid" || languageStr == "math" { 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, ), goldmark.WithParserOptions( @@ -167,7 +172,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) log.Error("Unable to ReadAll: %v", 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) return err } diff --git a/modules/markup/markdown/math/block_node.go b/modules/markup/markdown/math/block_node.go new file mode 100644 index 0000000000..bd8449babf --- /dev/null +++ b/modules/markup/markdown/math/block_node.go @@ -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 +} diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go new file mode 100644 index 0000000000..f865122886 --- /dev/null +++ b/modules/markup/markdown/math/block_parser.go @@ -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 +} diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go new file mode 100644 index 0000000000..d502065259 --- /dev/null +++ b/modules/markup/markdown/math/block_renderer.go @@ -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(`
`)
+		r.writeLines(w, source, n)
+	} else {
+		_, _ = w.WriteString(`
` + "\n") + } + return gast.WalkContinue, nil +} diff --git a/modules/markup/markdown/math/inline_node.go b/modules/markup/markdown/math/inline_node.go new file mode 100644 index 0000000000..245ff8dab0 --- /dev/null +++ b/modules/markup/markdown/math/inline_node.go @@ -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{}, + } +} diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go new file mode 100644 index 0000000000..0339674b6c --- /dev/null +++ b/modules/markup/markdown/math/inline_parser.go @@ -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) +} diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go new file mode 100644 index 0000000000..e4c0f3761d --- /dev/null +++ b/modules/markup/markdown/math/inline_renderer.go @@ -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(``) + 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(``) + return ast.WalkContinue, nil +} + +// RegisterFuncs registers the renderer for inline math nodes +func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindInline, r.renderInline) +} diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go new file mode 100644 index 0000000000..7854ac84db --- /dev/null +++ b/modules/markup/markdown/math/math.go @@ -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), + )) +} diff --git a/modules/markup/markdown/meta.go b/modules/markup/markdown/meta.go index faf92ae2c6..28913fd684 100644 --- a/modules/markup/markdown/meta.go +++ b/modules/markup/markdown/meta.go @@ -5,47 +5,101 @@ package markdown import ( + "bytes" "errors" - "strings" + "unicode" + "unicode/utf8" - "gopkg.in/yaml.v2" + "code.gitea.io/gitea/modules/log" + "gopkg.in/yaml.v3" ) -func isYAMLSeparator(line string) bool { - line = strings.TrimSpace(line) - for i := 0; i < len(line); i++ { - if line[i] != '-' { +func isYAMLSeparator(line []byte) bool { + idx := 0 + 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] != ' ' { + 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 len(line) > 2 + return true } // ExtractMetadata consumes a markdown file, parses YAML frontmatter, // and returns the frontmatter metadata separated from the markdown content func ExtractMetadata(contents string, out interface{}) (string, error) { - var front, body []string - lines := strings.Split(contents, "\n") - for idx, line := range lines { - if idx == 0 { - // First line has to be a separator - if !isYAMLSeparator(line) { - return "", errors.New("frontmatter must start with a separator line") - } - continue + body, err := ExtractMetadataBytes([]byte(contents), out) + return string(body), err +} + +// 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) { + 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) { - front, body = lines[1:idx], lines[idx+1:] + front = contents[frontMatterStart:start] + body = contents[end+1:] break } } 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 { - return "", err + log.Info("%s", string(front)) + + if err := yaml.Unmarshal(front, out); err != nil { + return contents, err } - return strings.Join(body, "\n"), nil + return body, nil } diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go index 939646f8fd..9332b35b42 100644 --- a/modules/markup/markdown/meta_test.go +++ b/modules/markup/markdown/meta_test.go @@ -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 ( sepTest = "-----" frontTest = `name: Test diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go index bef67e9e59..6a3b3a1bde 100644 --- a/modules/markup/markdown/renderconfig.go +++ b/modules/markup/markdown/renderconfig.go @@ -5,159 +5,114 @@ package markdown import ( - "fmt" "strings" + "code.gitea.io/gitea/modules/log" "github.com/yuin/goldmark/ast" - east "github.com/yuin/goldmark/extension/ast" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) // RenderConfig represents rendering configuration for this file type RenderConfig struct { - Meta string - Icon string - TOC bool - Lang string + Meta string + Icon string + TOC bool + Lang string + yamlNode *yaml.Node } -// ToRenderConfig converts a yaml.MapSlice to a RenderConfig -func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) { - if meta == nil { - return +// UnmarshalYAML implement yaml.v3 UnmarshalYAML +func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { + if rc == nil { + 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 { - switch v := giteaMetaControl.Value.(type) { - case string: - switch v { - case "none": - 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 - } - } - } + var basic basicRenderConfig + + err := value.Decode(&basic) + 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": + 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 { case "table": - return metaToTable(meta) + return nodeToTable(rc.yamlNode) case "details": - return metaToDetails(meta, rc.Icon) + return nodeToDetails(rc.yamlNode, rc.Icon) default: 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 -} diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go new file mode 100644 index 0000000000..1027035cda --- /dev/null +++ b/modules/markup/markdown/renderconfig_test.go @@ -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) + } + }) + } +} diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 57e88fdabc..807a8a7892 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -56,7 +56,7 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre") // 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 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") // 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. policy.AllowAttrs("style").OnElements("span", "p") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 09e510ffa0..1ac13cb5fe 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -344,10 +344,12 @@ var ( EnableHardLineBreakInDocuments bool CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` FileExtensions []string + EnableMath bool }{ EnableHardLineBreakInComments: true, EnableHardLineBreakInDocuments: false, FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), + EnableMath: true, } // Admin settings diff --git a/package-lock.json b/package-lock.json index b3597a624e..9e613422d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "font-awesome": "4.7.0", "jquery": "3.6.1", "jquery.are-you-sure": "1.9.0", + "katex": "0.16.2", "less": "4.1.3", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", @@ -7750,6 +7751,29 @@ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", "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": { "version": "2.0.0", "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", "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", diff --git a/package.json b/package.json index e2eaec4259..37571c01c2 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "font-awesome": "4.7.0", "jquery": "3.6.1", "jquery.are-you-sure": "1.9.0", + "katex": "0.16.2", "less": "4.1.3", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js index ef5067fd66..319c229385 100644 --- a/web_src/js/markup/content.js +++ b/web_src/js/markup/content.js @@ -1,10 +1,12 @@ import {renderMermaid} from './mermaid.js'; +import {renderMath} from './math.js'; import {renderCodeCopy} from './codecopy.js'; import {initMarkupTasklist} from './tasklist.js'; // code that runs for all markup content export function initMarkupContent() { renderMermaid(); + renderMath(); renderCodeCopy(); } diff --git a/web_src/js/markup/math.js b/web_src/js/markup/math.js new file mode 100644 index 0000000000..5790a327a5 --- /dev/null +++ b/web_src/js/markup/math.js @@ -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); + } + } +} diff --git a/web_src/less/animations.less b/web_src/less/animations.less index 92a3052a1f..ea31d53bfe 100644 --- a/web_src/less/animations.less +++ b/web_src/less/animations.less @@ -33,6 +33,13 @@ height: var(--height-loading); } +code.language-math.is-loading::after { + padding: 0; + border-width: 2px; + width: 1.25rem; + height: 1.25rem; +} + @keyframes fadein { 0% { opacity: 0; diff --git a/webpack.config.js b/webpack.config.js index 39628df049..3cc65d19d4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -37,6 +37,10 @@ const filterCssImport = (url, ...args) => { 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)) { return false; } From 5933f04094df2baa7e47d3d4372ad2fdfa2b6207 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 14 Sep 2022 13:24:39 +0200 Subject: [PATCH 4/9] Skip dirty check for team forms (#21154) The dirty check is not usefull for these forms. --- templates/org/team/members.tmpl | 2 +- templates/org/team/repositories.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index b9069c90de..ecb7830f18 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -9,7 +9,7 @@ {{template "org/team/navbar" .}} {{if .IsOrganizationOwner}}
-
+ {{.CsrfTokenHtml}}
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index d4c17d5b00..80bdf7b3db 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -11,7 +11,7 @@ {{if $canAddRemove}}
- + {{.CsrfTokenHtml}}
+
+ +
{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}
+
From bf325d44120c4a6fc7f67ff1dc25db770247c9a7 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Thu, 15 Sep 2022 15:25:16 +0200 Subject: [PATCH 9/9] Keep path when creating a new branch (#21153) If you are create a new new branch while viewing file or directory, you get redirected to the root of the repo. With this PR, you keep your current path instead of getting redirected to the repo root. --- routers/web/repo/branch.go | 2 +- services/forms/repo_branch_form.go | 1 + templates/repo/branch_dropdown.tmpl | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index d14ba6cbe9..d1f1255db4 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -427,5 +427,5 @@ func CreateBranch(ctx *context.Context) { } 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)) } diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go index f9262aaede..011926092f 100644 --- a/services/forms/repo_branch_form.go +++ b/services/forms/repo_branch_form.go @@ -16,6 +16,7 @@ import ( // NewBranchForm form for creating a new branch type NewBranchForm struct { NewBranchName string `binding:"Required;MaxSize(100);GitRefName"` + CurrentPath string CreateTag bool } diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index 9d1ec10d3c..2010fe261e 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -94,6 +94,9 @@ {{.root.CsrfTokenHtml}} + {{if $.root.TreePath}} + + {{end}}