fix(ui): reworked file preview placement towards better HTML validity (#9181)

### What?

- fixes HTML nodes placement for inline previews
- (breaking?) disallows file previews in headings (`<h1>`, …), striked out (`<del>`), `<summary>` and other environments
- allows them in `<span>`, `<em>` and `<strong>` environments, but without extra formatting
- allows them in `<div>`, `<li>`, `<th>`, `<td>` and `<details>` (not in `<summary>`) environments following the parent’s formatting
- improves overall HTML validity, but only to the extend of the direct parent nodes (but not a big issue, as modern browsers tend to ignore invalid HTML and still parse it)
- fix #9136

See examples of strangely formatted file previews at https://v13.next.forgejo.org/mahlzahn/test/issues/4.

### How is it implemented?

For links in `<p>`, `<span>`, `<em>` and `<strong>` parent nodes it inserts the file preview and following text as siblings to the **parent** node.
`<em>before LINK after</em>`
→ `<em>before </em>PREVIEW<em> after</em>`

For links in `<div>`, `<li>`, `<th>`, `<td>` and `<details>` parent nodes it inserts the file preview and following text as siblings to the **text** node.
`<div>before LINK after</div>`
→ `<div>before PREVIEW after</div>`

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9181
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Robert Wolff <mahlzahn@posteo.de>
Co-committed-by: Robert Wolff <mahlzahn@posteo.de>
This commit is contained in:
Robert Wolff 2025-09-15 15:57:52 +02:00 committed by Gusted
commit 14c5462019
2 changed files with 102 additions and 38 deletions

View file

@ -1078,12 +1078,15 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling
for node != nil && node != next {
if node.Parent == nil || node.Parent.Type != html.ElementNode {
node = node.NextSibling
continue
}
previews := NewFilePreviews(ctx, node, locale)
if previews == nil {
node = node.NextSibling
continue
}
offset := 0
for _, preview := range previews {
previewNode := preview.CreateHTML(locale)
@ -1091,23 +1094,41 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
// Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
before := node.Data[:(preview.start - offset)]
after := node.Data[(preview.end - offset):]
afterPrefix := "<p>"
offset = preview.end - len(afterPrefix)
node.Data = before
nextSibling := node.NextSibling
node.Parent.InsertBefore(&html.Node{
Type: html.RawNode,
Data: "</p>",
}, nextSibling)
node.Parent.InsertBefore(previewNode, nextSibling)
afterNode := &html.Node{
Type: html.RawNode,
Data: afterPrefix + after,
Type: html.TextNode,
Data: after,
}
matched := true
switch node.Parent.Data {
case "div", "li", "td", "th", "details":
nextSibling := node.NextSibling
node.Parent.InsertBefore(previewNode, nextSibling)
node.Parent.InsertBefore(afterNode, nextSibling)
case "p", "span", "em", "strong":
nextSibling := node.Parent.NextSibling
node.Parent.Parent.InsertBefore(previewNode, nextSibling)
afterPNode := &html.Node{
Type: html.ElementNode,
Data: node.Parent.Data,
Attr: node.Parent.Attr,
}
afterPNode.AppendChild(afterNode)
node.Parent.Parent.InsertBefore(afterPNode, nextSibling)
siblingNode := node.NextSibling
if siblingNode != nil {
node.NextSibling = nil
siblingNode.PrevSibling = nil
afterPNode.AppendChild(siblingNode)
}
default:
matched = false
}
if matched {
offset = preview.end
node.Data = before
node = afterNode
}
node.Parent.InsertBefore(afterNode, nextSibling)
node = afterNode
}
node = node.NextSibling
}
}