diff --git a/modules/markup/common/footnote.go b/modules/markup/common/footnote.go index 2c47ea13859..ef89a7046a3 100644 --- a/modules/markup/common/footnote.go +++ b/modules/markup/common/footnote.go @@ -401,8 +401,10 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt _, _ = w.WriteString(`">`) // FIXME: here and below, need to keep the classes + // literal brackets separate adjacent refs, also where no CSS applies like in mails + _, _ = w.WriteString("[") _, _ = w.WriteString(is) - _, _ = w.WriteString(` `) // the style doesn't work at the moment, so add a space to separate the names + _, _ = w.WriteString("]") } return ast.WalkContinue, nil } diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go index 6ed0df082be..9e19018b278 100644 --- a/modules/markup/html_issue_test.go +++ b/modules/markup/html_issue_test.go @@ -55,7 +55,7 @@ func TestRender_IssueList(t *testing.T) { `}, { "IssueFootnote", "foo[^1][^2]\n\n[^1]: bar\n[^2]: baz", - `

foo1 2

+ `

foo[1][2]


    diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index fae9d06e2a9..19200b2d32d 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -5,7 +5,6 @@ package markdown import ( "html/template" - "strconv" "github.com/yuin/goldmark/ast" ) @@ -56,36 +55,6 @@ func NewSummary() *Summary { return &Summary{} } -// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox -type TaskCheckBoxListItem struct { - *ast.ListItem - IsChecked bool - SourcePosition int -} - -// KindTaskCheckBoxListItem is the NodeKind for TaskCheckBoxListItem -var KindTaskCheckBoxListItem = ast.NewNodeKind("TaskCheckBoxListItem") - -// Dump implements Node.Dump . -func (n *TaskCheckBoxListItem) Dump(source []byte, level int) { - m := map[string]string{} - m["IsChecked"] = strconv.FormatBool(n.IsChecked) - m["SourcePosition"] = strconv.FormatInt(int64(n.SourcePosition), 10) - ast.DumpHelper(n, source, level, m, nil) -} - -// Kind implements Node.Kind. -func (n *TaskCheckBoxListItem) Kind() ast.NodeKind { - return KindTaskCheckBoxListItem -} - -// NewTaskCheckBoxListItem returns a new TaskCheckBoxListItem node. -func NewTaskCheckBoxListItem(listItem *ast.ListItem) *TaskCheckBoxListItem { - return &TaskCheckBoxListItem{ - ListItem: listItem, - } -} - // Icon is an inline for a Fomantic UI icon type Icon struct { ast.BaseInline diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index f30116ba009..2d2daa6cc1d 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -146,7 +146,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(ast.KindCodeSpan, r.renderCodeSpan) reg.Register(ast.KindCodeBlock, r.renderCodeBlock) reg.Register(KindAttention, r.renderAttention) - reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) reg.Register(KindRawHTML, r.renderRawHTML) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 259613b12c5..e9dd6cb1267 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -125,6 +125,10 @@ Here are some links to the most important topics. You can find the full list of - [x] checked - [ ] still unchecked +* [x] loose + + second paragraph + ### Definition list First Term @@ -209,6 +213,12 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
  1. checked
  2. still unchecked
  3. +

    Definition list

    First Term
    @@ -218,7 +228,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
    This is another definition of the second term.

    Footnotes

    -

    Here is a simple footnote,1 and here is a longer one.2

    +

    Here is a simple footnote,[1] and here is a longer one.[2]


      diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go index 05054a46878..181d5d0addf 100644 --- a/modules/markup/markdown/transform_list.go +++ b/modules/markup/markdown/transform_list.go @@ -4,7 +4,7 @@ package markdown import ( - "fmt" + "strconv" "gitea.dev/modules/markup" @@ -14,71 +14,37 @@ import ( "github.com/yuin/goldmark/util" ) -func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - n := node.(*TaskCheckBoxListItem) //nolint:forcetypeassert // registered for KindTaskCheckBoxListItem only - if entering { - if n.Attributes() != nil { - _, _ = w.WriteString("') - } else { - _, _ = w.WriteString("
    1. ") - } - fmt.Fprintf(w, ``) - } else { - _ = w.WriteByte('>') - } - fc := n.FirstChild() - if fc != nil { - if _, ok := fc.(*ast.TextBlock); !ok { - _ = w.WriteByte('\n') - } - } +func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + checkBox, ok := node.(*east.TaskCheckBox) + if !entering || !ok { + return ast.WalkContinue, nil + } + _, _ = w.WriteString(``) } else { - _, _ = w.WriteString("
    2. \n") + _ = w.WriteByte('>') } return ast.WalkContinue, nil } -func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - return ast.WalkContinue, nil -} - func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc *RenderConfig) { - if v.HasChildren() { - children := make([]ast.Node, 0, v.ChildCount()) - child := v.FirstChild() - for child != nil { - children = append(children, child) - child = child.NextSibling() + for listItem := v.FirstChild(); listItem != nil; listItem = listItem.NextSibling() { + block := listItem.FirstChild() + if block == nil { + continue } - v.RemoveChildren(v) - - for _, child := range children { - listItem := child.(*ast.ListItem) //nolint:forcetypeassert // a list only holds list items - if !child.HasChildren() || !child.FirstChild().HasChildren() { - v.AppendChild(v, child) - continue - } - taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox) - if !ok { - v.AppendChild(v, child) - continue - } - newChild := NewTaskCheckBoxListItem(listItem) - newChild.IsChecked = taskCheckBox.IsChecked - newChild.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("task-list-item"))) - segments := newChild.FirstChild().Lines() - if segments.Len() > 0 { - segment := segments.At(0) - newChild.SourcePosition = rc.metaLength + segment.Start - } - v.AppendChild(v, newChild) + checkBox, ok := block.FirstChild().(*east.TaskCheckBox) + if !ok { + continue + } + listItem.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("task-list-item"))) + if lines := block.Lines(); lines.Len() > 0 { + checkBox.SetAttributeString("data-source-position", []byte(strconv.Itoa(rc.metaLength+lines.At(0).Start))) } } diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index e5e1ff385bf..b0fdcbe641c 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -21,6 +21,8 @@ import ( "gitea.dev/modules/charset" "gitea.dev/modules/git" "gitea.dev/modules/indexer/code" + "gitea.dev/modules/markup" + "gitea.dev/modules/markup/markdown" "gitea.dev/modules/templates" "gitea.dev/modules/util" "gitea.dev/services/context" @@ -35,8 +37,8 @@ func List(ctx *context.Context) { } var subNames []string for _, tmplName := range templateNames { - subName := strings.TrimSuffix(tmplName, ".tmpl") - if !strings.HasPrefix(subName, "devtest-") { + subName, isTmpl := strings.CutSuffix(tmplName, ".tmpl") + if isTmpl && !strings.HasPrefix(subName, "devtest-") { subNames = append(subNames, subName) } } @@ -213,6 +215,19 @@ func prepareMockDataAvatarStack(ctx *context.Context) { */ } +func prepareMockDataMarkdown(ctx *context.Context) { + content, err := templates.AssetFS().ReadFile("devtest/markdown.md") + if err != nil { + ctx.ServerError("ReadFile", err) + return + } + rctx := markup.NewRenderContext(ctx).WithMetas(markup.ComposeSimpleDocumentMetas()).WithEnableHeadingIDGeneration(true) + ctx.Data["MarkdownHTML"], err = markdown.RenderString(rctx, string(content)) + if err != nil { + ctx.ServerError("RenderString", err) + } +} + func prepareMockDataRelativeTime(ctx *context.Context) { now := time.Now() ctx.Data["TimeNow"] = now @@ -244,6 +259,8 @@ func prepareMockData(ctx *context.Context) { prepareMockDataBadgeCommitSign(ctx) case "/devtest/badge-actions-svg": prepareMockDataBadgeActionsSvg(ctx) + case "/devtest/markdown": + prepareMockDataMarkdown(ctx) case "/devtest/relative-time": prepareMockDataRelativeTime(ctx) case "/devtest/toast-and-message": diff --git a/templates/devtest/markdown.md b/templates/devtest/markdown.md new file mode 100644 index 00000000000..240fdd2dc56 --- /dev/null +++ b/templates/devtest/markdown.md @@ -0,0 +1,237 @@ +# Markdown + +## Text + +### Inline elements + +Text with **bold**, *italic*, ~~strikethrough~~, `inline code`, [a link](https://gitea.com), an autolink https://gitea.com and Ctrl+C. + +H2O, x2, marked, inserted, HTML, sample output, variable, small, quote and emoji :+1: :tada:. + +### Paragraphs + +A paragraph with a soft line break +continues on the same line, while a hard break\ +starts a new line. + +A second paragraph that is long enough to wrap onto multiple lines, so that the line height of paragraphs can be compared with the line height of list items further below. + +### Headings + +# Heading 1 with `code` +## Heading 2 with `code` +### Heading 3 with `code` +#### Heading 4 with `code` +##### Heading 5 with `code` +###### Heading 6 with `code` + +### Horizontal rule + +Text above the rule. + +--- + +Text below the rule. + +## Quotes + +### Blockquote + +> A blockquote with a paragraph. +> +> > A nested blockquote. +> +> - A list in a blockquote +> - Another item +> +> ``` +> code in a blockquote +> ``` + +### Alerts + +> [!NOTE] +> Useful information that users should know, even when skimming content. + +> [!TIP] +> Helpful advice for doing things better or more easily. + +> [!IMPORTANT] +> Key information users need to know to achieve their goal. + +> [!WARNING] +> Urgent info that needs immediate user attention to avoid problems. + +> [!CAUTION] +> Advises about risks or negative outcomes of certain actions. +> +> A second paragraph in an alert. + +## Lists + +### Tight list + +- Item one +- Item two + - Nested item + - Deeper nested item +- Item three + +### Loose list + +- Item one + +- Item two with a second paragraph + + The second paragraph. + +- Item three + +### Ordered list + +1. Item one +2. Item two + 1. Nested item + 1. Deeper nested item +3. Item three + +### Ordered list with type + +
        +
      1. Item one +
          +
        1. Nested item with type="a"
        2. +
        3. Another nested item
        4. +
        +
      2. +
      + +### Ordered list with start + +5. Item five +6. Item six + +### Code block in list + +1. A tight item with a code block: + ```js + const a = 1; + ``` +2. Item after the code block + +### Task list + +- [ ] Unchecked task +- [x] Checked task + - [ ] Nested task +- Normal item + +1. [ ] Ordered task +2. [x] Ordered checked task + +### Loose task list + +- [ ] Loose task + +- [x] Loose checked task + + A second paragraph in a loose task. + +### Right-to-left task list + +- [ ] משימה + +- [x] משימה שהושלמה + +## Code + +### Code blocks + +```go +package main + +func main() { + println("hello") +} +``` + +``` +A plain code block with a long line that needs to scroll horizontally: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef +``` + + An indented code block + +### Math + +Inline math $E = mc^2$ in a sentence. + +$$ +\sum_{i=1}^n x_i +$$ + +## Tables + +### Table + +| Left | Center | Right | +| :--- | :---: | ---: | +| `code` | 1 | 1.00 | +| text | 22 | 22.00 | +| longer cell content | 333 | 333.00 | +| logo image | 4444 | 4444.00 | + +### Table with paragraphs + + + + +
      HeaderHeader

      Paragraph in a cell

      Text in a cell
      + +## Other + +### Details + +
      +Closed details + +Hidden content. + +
      + +
      +Open details + +Visible content. + +
      + +
      +

      Heading in summary

      + +Hidden content. + +
      + +### Definition list + +
      +
      Term
      +
      Definition
      +
      First term
      +
      Second term
      +
      Shared definition
      +
      + +### Images + +An inline image ![logo](/assets/img/logo.svg) in text. + +

      centered logo

      + +### Footnotes + +A sentence with a footnote[^1] and another one[^2][^3]. + +[^1]: The first footnote. +[^2]: The second footnote with **bold** text. +[^3]: The third footnote. diff --git a/templates/devtest/markdown.tmpl b/templates/devtest/markdown.tmpl new file mode 100644 index 00000000000..b8b633e6c98 --- /dev/null +++ b/templates/devtest/markdown.tmpl @@ -0,0 +1,9 @@ +{{template "devtest/devtest-header"}} +
      +
      +
      + {{.MarkdownHTML}} +
      +
      +
      +{{template "devtest/devtest-footer"}} diff --git a/web_src/css/base.css b/web_src/css/base.css index e594b40b692..ed8e1250ddb 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -108,6 +108,11 @@ samp, font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */ } +mark { + background: var(--color-highlight-bg); + color: inherit; +} + @font-face { font-family: gitea-math-book; /* the Book weight shares its family name with the thinner Regular */ src: local("NewComputerModernMath-Book"), local("NewCMMath-Book"); @@ -456,53 +461,6 @@ a.label, color: var(--color-text) !important; } -.color-preview { - display: inline-block; - margin-left: 0.4em; - height: 0.67em; - width: 0.67em; - border-radius: var(--border-radius); -} - -.attention-icon { - margin: 2px 6px 0 0; -} - -blockquote.attention-note { - border-left-color: var(--color-info-text); -} -strong.attention-note, svg.attention-note { - color: var(--color-info-text); -} - -blockquote.attention-tip { - border-left-color: var(--color-success-text); -} -strong.attention-tip, svg.attention-tip { - color: var(--color-success-text); -} - -blockquote.attention-important { - border-left-color: var(--color-priority-text); -} -strong.attention-important, svg.attention-important { - color: var(--color-priority-text); -} - -blockquote.attention-warning { - border-left-color: var(--color-warning-text); -} -strong.attention-warning, svg.attention-warning { - color: var(--color-warning-text); -} - -blockquote.attention-caution { - border-left-color: var(--color-error-text); -} -strong.attention-caution, svg.attention-caution { - color: var(--color-error-text); -} - /* FIXME: this is a longstanding dirty patch since 2015, it only makes the pages more messy and shouldn't be used */ .center { text-align: center; diff --git a/web_src/css/index.css b/web_src/css/index.css index 71e58e7c8ec..0ebd3bdae08 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -49,6 +49,7 @@ @import "./features/console.css"; @import "./features/captcha.css"; +@import "./markup/attention.css"; @import "./markup/content.css"; @import "./markup/codeblock.css"; @import "./markup/codepreview.css"; diff --git a/web_src/css/markup/attention.css b/web_src/css/markup/attention.css new file mode 100644 index 00000000000..a28e352e620 --- /dev/null +++ b/web_src/css/markup/attention.css @@ -0,0 +1,43 @@ +.attention-header > p:first-child { + display: flex; + align-items: center; +} + +.attention-icon { + margin-inline-end: 6px; +} + +blockquote.attention-note { + border-left-color: var(--color-info-text); +} +strong.attention-note, svg.attention-note { + color: var(--color-info-text); +} + +blockquote.attention-tip { + border-left-color: var(--color-success-text); +} +strong.attention-tip, svg.attention-tip { + color: var(--color-success-text); +} + +blockquote.attention-important { + border-left-color: var(--color-priority-text); +} +strong.attention-important, svg.attention-important { + color: var(--color-priority-text); +} + +blockquote.attention-warning { + border-left-color: var(--color-warning-text); +} +strong.attention-warning, svg.attention-warning { + color: var(--color-warning-text); +} + +blockquote.attention-caution { + border-left-color: var(--color-error-text); +} +strong.attention-caution, svg.attention-caution { + color: var(--color-error-text); +} diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 10894c69d61..bfd94fd9544 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -86,6 +86,7 @@ .markup h5 code, .markup h6 tt, .markup h6 code { + padding: 0 0.2em; font-size: inherit; } @@ -118,6 +119,16 @@ color: var(--color-text-light-2); } +.markup summary :is(h1, h2, h3, h4, h5, h6) { + display: inline-block; + padding-bottom: 0; + border-bottom: 0; +} + +.markup summary .anchor { + margin-inline-start: -40px; /* clear the disclosure marker */ +} + .markup p, .markup blockquote, .markup details, @@ -138,8 +149,12 @@ In markup content, we always use bottom margin for all elements */ margin-bottom: 16px; } +.markup p { + line-height: inherit; /* override base.css */ +} + .markup hr { - height: 4px; + height: 1px; padding: 0; margin: 16px 0; background-color: var(--color-secondary); @@ -159,10 +174,6 @@ In markup content, we always use bottom margin for all elements */ margin-top: 4px; } -.markup .task-list-item p + ul { - margin-top: 16px; -} - .markup .task-list-item input[type="checkbox"] { margin-bottom: 0.25em; margin-inline: -1.4em 0.6em; @@ -170,13 +181,10 @@ In markup content, we always use bottom margin for all elements */ padding: 0; } -.markup .task-list-item input[type="checkbox"] + p { - margin-inline-start: -0.2em; - display: inline; -} - -.markup .task-list-item > p { - margin-inline: 16px; +/* a paragraph with another direction than its list has no list padding to hang the checkbox into */ +.markup :is(ul, ol):dir(ltr) > .task-list-item > p:dir(rtl) > input[type="checkbox"]:first-child, +.markup :is(ul, ol):dir(rtl) > .task-list-item > p:dir(ltr) > input[type="checkbox"]:first-child { + margin-inline-start: 0; } .markup .task-list-item + .task-list-item { @@ -200,11 +208,14 @@ In markup content, we always use bottom margin for all elements */ margin-bottom: 0; } -.markup ol ol, -.markup ul ol { +.markup :is(ol, ul) ol:not([type]) { list-style-type: lower-roman; } +.markup :is(ol, ul) :is(ol, ul) ol:not([type]) { + list-style-type: lower-alpha; +} + .markup li > p { margin-top: 16px; } @@ -225,13 +236,18 @@ In markup content, we always use bottom margin for all elements */ font-weight: var(--font-weight-semibold); } +.markup dl dt + dt { + margin-top: 0; +} + .markup dl dd { padding: 0 16px; + margin-inline-start: 0; margin-bottom: 16px; } .markup blockquote { - margin-inline-start: 0; + margin-inline: 0; padding: 0 15px; color: var(--color-text-light-2); border-left: 0.25em solid var(--color-secondary); @@ -250,6 +266,11 @@ In markup content, we always use bottom margin for all elements */ width: 100%; max-width: 100%; overflow: auto; + font-variant: tabular-nums; +} + +.markup table td > :last-child { + margin-bottom: 0; } .markup table th { @@ -297,17 +318,8 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { padding: 0; /* let the file-not-rendered-prompt layout itself */ } -/* this background ensures images can break
      . We can only do this on - cases where the background is known and not transparent. */ -.file-view.markup img, -.file-view.markup video, -.comment-body .markup img, /* regular comment */ -.comment-body .markup video, -.comment-content .markup img, /* code comment */ -.comment-content .markup video, -.wiki .markup img, -.wiki .markup video { - background: var(--color-box-body); +.markup :is(h1, h2, hr) { + display: flow-root; /* keep borders clear of floated images */ } .markup img[align="right"], @@ -401,6 +413,7 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { background-color: var(--color-markup-code-inline); border: 1px solid var(--color-secondary); border-radius: var(--border-radius); + max-width: 100%; } .markup details.frontmatter-content summary { @@ -511,3 +524,11 @@ the manually written code-block elements on "packages" pages don't have the cont .markup .auto-hide-control .ui.button:active { background: var(--color-secondary-dark-1) !important; } + +.markup .color-preview { + display: inline-block; + margin-left: 0.4em; + height: 0.67em; + width: 0.67em; + border-radius: var(--border-radius); +}