Skip to content

Commit e61c09e

Browse files
authored
Add loading spinners and mermaid error handling (#12358)
- Add loading spinners on editor and mermaid renderers - Add error handling and inline error box for mermaid - Fix Mermaid rendering by using the .init api
1 parent 5e5c893 commit e61c09e

10 files changed

Lines changed: 148 additions & 27 deletions

File tree

modules/markup/markdown/markdown.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package markdown
77

88
import (
99
"bytes"
10+
"strings"
1011
"sync"
1112

1213
"code.gitea.io/gitea/modules/log"
@@ -57,13 +58,33 @@ func render(body []byte, urlPrefix string, metas map[string]string, wikiMarkdown
5758
chromahtml.PreventSurroundingPre(true),
5859
),
5960
highlighting.WithWrapperRenderer(func(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
60-
language, _ := c.Language()
61-
if language == nil {
62-
language = []byte("text")
63-
}
6461
if entering {
62+
language, _ := c.Language()
63+
if language == nil {
64+
language = []byte("text")
65+
}
66+
67+
languageStr := string(language)
68+
69+
preClasses := []string{}
70+
if languageStr == "mermaid" {
71+
preClasses = append(preClasses, "is-loading")
72+
}
73+
74+
if len(preClasses) > 0 {
75+
_, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`)
76+
if err != nil {
77+
return
78+
}
79+
} else {
80+
_, err := w.WriteString(`<pre>`)
81+
if err != nil {
82+
return
83+
}
84+
}
85+
6586
// include language-x class as part of commonmark spec
66-
_, err := w.WriteString("<pre><code class=\"chroma language-" + string(language) + "\">")
87+
_, err := w.WriteString(`<code class="chroma language-` + string(language) + `">`)
6788
if err != nil {
6889
return
6990
}

modules/markup/sanitizer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func NewSanitizer() {
3838
func ReplaceSanitizer() {
3939
sanitizer.policy = bluemonday.UGCPolicy()
4040
// For Chroma markdown plugin
41+
sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre")
4142
sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
4243

4344
// Checkboxes

templates/repo/editor/edit.tmpl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@
4141
data-markdown-file-exts="{{.MarkdownFileExts}}"
4242
data-line-wrap-extensions="{{.LineWrapExtensions}}">
4343
{{.FileContent}}</textarea>
44-
<div class="editor-loading">
45-
{{.i18n.Tr "loading"}}
46-
</div>
44+
<div class="editor-loading is-loading"></div>
4745
</div>
4846
<div class="ui bottom attached tab segment markdown" data-tab="preview">
4947
{{.i18n.Tr "loading"}}

web_src/js/markdown/content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {renderMermaid} from './mermaid.js';
22

33
export default async function renderMarkdownContent() {
4-
await renderMermaid(document.querySelectorAll('.language-mermaid'));
4+
await renderMermaid(document.querySelectorAll('code.language-mermaid'));
55
}

web_src/js/markdown/mermaid.js

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,56 @@
1-
import {random} from '../utils.js';
1+
const MAX_SOURCE_CHARACTERS = 5000;
2+
3+
function displayError(el, err) {
4+
el.closest('pre').classList.remove('is-loading');
5+
const errorNode = document.createElement('div');
6+
errorNode.setAttribute('class', 'ui message error markdown-block-error mono');
7+
errorNode.textContent = err.str || err.message || String(err);
8+
el.closest('pre').before(errorNode);
9+
}
210

311
export async function renderMermaid(els) {
412
if (!els || !els.length) return;
513

6-
const {mermaidAPI} = await import(/* webpackChunkName: "mermaid" */'mermaid');
14+
const mermaid = await import(/* webpackChunkName: "mermaid" */'mermaid');
715

8-
mermaidAPI.initialize({
9-
startOnLoad: false,
16+
mermaid.initialize({
17+
mermaid: {
18+
startOnLoad: false,
19+
},
20+
flowchart: {
21+
useMaxWidth: true,
22+
htmlLabels: false,
23+
},
1024
theme: 'neutral',
1125
securityLevel: 'strict',
1226
});
1327

1428
for (const el of els) {
15-
mermaidAPI.render(`mermaid-${random(12)}`, el.textContent, (svg, bindFunctions) => {
16-
const div = document.createElement('div');
17-
div.classList.add('mermaid-chart');
18-
div.innerHTML = svg;
19-
if (typeof bindFunctions === 'function') bindFunctions(div);
20-
el.closest('pre').replaceWith(div);
21-
});
29+
if (el.textContent.length > MAX_SOURCE_CHARACTERS) {
30+
displayError(el, new Error(`Mermaid source of ${el.textContent.length} characters exceeds the maximum allowed length of ${MAX_SOURCE_CHARACTERS}.`));
31+
continue;
32+
}
33+
34+
let valid;
35+
try {
36+
valid = mermaid.parse(el.textContent);
37+
} catch (err) {
38+
displayError(el, err);
39+
}
40+
41+
if (!valid) {
42+
el.closest('pre').classList.remove('is-loading');
43+
continue;
44+
}
45+
46+
try {
47+
mermaid.init(undefined, el, (id) => {
48+
const svg = document.getElementById(id);
49+
svg.classList.add('mermaid-chart');
50+
svg.closest('pre').replaceWith(svg);
51+
});
52+
} catch (err) {
53+
displayError(el, err);
54+
}
2255
}
2356
}

web_src/less/_markdown.less

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -495,10 +495,20 @@
495495
}
496496
}
497497

498-
.mermaid-chart {
499-
display: flex;
500-
justify-content: center;
501-
align-items: center;
502-
padding: 1rem;
503-
margin: 1rem 0;
498+
.markdown-block-error {
499+
margin-bottom: 0 !important;
500+
border-bottom-left-radius: 0 !important;
501+
border-bottom-right-radius: 0 !important;
502+
box-shadow: none !important;
503+
font-size: 85% !important;
504+
white-space: pre !important;
505+
padding: .5rem 1rem !important;
506+
text-align: left !important;
507+
}
508+
509+
.markdown-block-error + pre {
510+
border-top: none !important;
511+
margin-top: 0 !important;
512+
border-top-left-radius: 0 !important;
513+
border-top-right-radius: 0 !important;
504514
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@keyframes isloadingspin {
2+
0% { transform: translate(-50%, -50%) rotate(0deg); }
3+
100% { transform: translate(-50%, -50%) rotate(360deg); }
4+
}
5+
6+
.is-loading {
7+
background: transparent !important;
8+
color: transparent !important;
9+
border: transparent !important;
10+
pointer-events: none !important;
11+
position: relative !important;
12+
overflow: hidden !important;
13+
}
14+
15+
.is-loading:after {
16+
content: "";
17+
position: absolute;
18+
display: block;
19+
width: 4rem;
20+
height: 4rem;
21+
left: 50%;
22+
top: 50%;
23+
transform: translate(-50%, -50%);
24+
animation: isloadingspin 500ms infinite linear;
25+
border-width: 4px;
26+
border-style: solid;
27+
border-color: #ececec #ececec #666 #666;
28+
border-radius: 100%;
29+
}
30+
31+
.markdown pre.is-loading,
32+
.editor-loading.is-loading {
33+
height: 12rem;
34+
}

web_src/less/index.less

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@import "~font-awesome/css/font-awesome.css";
22
@import "./vendor/gitGraph.css";
3+
@import "./features/animations.less";
4+
@import "./markdown/mermaid.less";
35

46
@import "_svg";
57
@import "_tribute";

web_src/less/markdown/mermaid.less

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.mermaid-chart {
2+
display: flex;
3+
justify-content: center;
4+
align-items: center;
5+
padding: 1rem;
6+
margin: 1rem 0;
7+
}
8+
9+
/* mermaid's errorRenderer seems to unavoidably spew stuff into <body>, hide it */
10+
body > div[id*="mermaid-"] {
11+
display: none !important;
12+
}

web_src/less/themes/theme-arc-green.less

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1260,7 +1260,8 @@ input {
12601260
border-color: #794f31;
12611261
}
12621262

1263-
.ui.red.message {
1263+
.ui.red.message,
1264+
.ui.error.message {
12641265
background-color: rgba(80, 23, 17, .6);
12651266
color: #f9cbcb;
12661267
box-shadow: 0 0 0 1px rgba(121, 71, 66, .5) inset, 0 0 0 0 transparent;
@@ -1923,3 +1924,12 @@ footer .container .links > * {
19231924
.mermaid-chart {
19241925
filter: invert(84%) hue-rotate(180deg);
19251926
}
1927+
1928+
.is-loading:after {
1929+
border-color: #4a4c58 #4a4c58 #d7d7da #d7d7da;
1930+
}
1931+
1932+
.markdown-block-error {
1933+
border: 1px solid rgba(121, 71, 66, .5) !important;
1934+
border-bottom: none !important;
1935+
}

0 commit comments

Comments
 (0)