mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 02:08:13 +00:00
forgejo: complete patch directory with apply instructions
Add the missing Go source files and a README covering the full apply procedure. Previously only custom assets and the test file were tracked here; ifc_url.go and the two-file diff were only in the Forgejo tree. - forgejo/modules/markup/markdown/ifc_url.go — goldmark extension source - forgejo/go.patch — diff for markdown.go + markup.go (7 lines total) - forgejo/README.md — step-by-step: apply patch, run tests, build, deploy assets, configure app.ini, upgrade procedure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
995218bb4e
commit
e02ba0e214
3 changed files with 445 additions and 0 deletions
145
forgejo/README.md
Normal file
145
forgejo/README.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Forgejo integration
|
||||
|
||||
This directory contains the Forgejo source patch and custom assets for the
|
||||
ifcurl preview extension. The integration adds:
|
||||
|
||||
- **Markdown preview** — ifc:// URLs in markdown render as inline `<figure>`
|
||||
preview images fetched from the ifcurl preview service.
|
||||
- **"View in 3D" button** — appears on `.ifc` file view pages alongside Raw /
|
||||
Permalink / History.
|
||||
- **Browser viewer** — a self-contained WebGL IFC viewer served as a Forgejo
|
||||
custom asset.
|
||||
|
||||
---
|
||||
|
||||
## Directory layout
|
||||
|
||||
```
|
||||
forgejo/
|
||||
go.patch ← diff against Forgejo source (apply once)
|
||||
modules/markup/markdown/
|
||||
ifc_url.go ← new Go source file (copy into source tree)
|
||||
ifc_url_test.go ← Go tests (copy into source tree)
|
||||
custom/public/assets/
|
||||
viewer.html ← browser IFC viewer (no rebuild needed)
|
||||
viewer-url.js ← viewer URL logic module
|
||||
templates/custom/
|
||||
footer.tmpl ← "View in 3D" button injection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Forgejo source tree cloned and building cleanly
|
||||
- `go` 1.21+ in PATH
|
||||
- ifcurl preview service running and reachable from the Forgejo server
|
||||
|
||||
The patch was written against the `v13.0` branch of Forgejo. Check which
|
||||
Forgejo commit the patch was authored against with:
|
||||
|
||||
```bash
|
||||
cd /path/to/forgejo
|
||||
git log --oneline modules/markup/markdown/markdown.go | head -3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Applying the patch
|
||||
|
||||
### 1. Copy new Go source files
|
||||
|
||||
```bash
|
||||
cp forgejo/modules/markup/markdown/ifc_url.go /path/to/forgejo/modules/markup/markdown/
|
||||
cp forgejo/modules/markup/markdown/ifc_url_test.go /path/to/forgejo/modules/markup/markdown/
|
||||
```
|
||||
|
||||
### 2. Apply the diff to existing files
|
||||
|
||||
```bash
|
||||
cd /path/to/forgejo
|
||||
git apply /path/to/ifcurl/forgejo/go.patch
|
||||
```
|
||||
|
||||
This adds one line to `modules/markup/markdown/markdown.go` and six lines to
|
||||
`modules/setting/markup.go`. If the patch does not apply cleanly (e.g. after
|
||||
a Forgejo upstream upgrade), the changes are small enough to apply by hand —
|
||||
see `go.patch` for the exact hunks.
|
||||
|
||||
### 3. Run the Go tests
|
||||
|
||||
```bash
|
||||
cd /path/to/forgejo
|
||||
go test ./modules/markup/markdown/ -run TestIfcURL -v
|
||||
```
|
||||
|
||||
All seven tests should pass before proceeding.
|
||||
|
||||
### 4. Build and deploy Forgejo
|
||||
|
||||
```bash
|
||||
cd /path/to/forgejo
|
||||
go build \
|
||||
-tags 'sqlite sqlite_unlock_notify' \
|
||||
-ldflags "-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'" \
|
||||
-o forgejo .
|
||||
|
||||
sudo cp forgejo /usr/bin/forgejo
|
||||
sudo systemctl restart forgejo
|
||||
```
|
||||
|
||||
Adjust build tags and `-ldflags` to match your existing Forgejo build
|
||||
configuration.
|
||||
|
||||
---
|
||||
|
||||
## Deploying custom assets (no rebuild required)
|
||||
|
||||
These files can be updated at any time without recompiling Forgejo.
|
||||
|
||||
### Viewer and URL logic
|
||||
|
||||
```bash
|
||||
sudo cp forgejo/custom/public/assets/viewer.html /etc/forgejo/public/assets/
|
||||
sudo cp forgejo/custom/public/assets/viewer-url.js /etc/forgejo/public/assets/
|
||||
```
|
||||
|
||||
Served at `/assets/viewer.html` and `/assets/viewer-url.js`.
|
||||
|
||||
### "View in 3D" footer template
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/forgejo/custom/templates/custom/
|
||||
sudo cp forgejo/templates/custom/footer.tmpl /var/lib/forgejo/custom/templates/custom/
|
||||
sudo systemctl restart forgejo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to `/etc/forgejo/conf/app.ini`:
|
||||
|
||||
```ini
|
||||
[ifcurl]
|
||||
PREVIEW_SERVICE_URL = http://localhost:8000
|
||||
```
|
||||
|
||||
Set `PREVIEW_SERVICE_URL` to the base URL of the running ifcurl preview
|
||||
service. If left empty, ifc:// links in markdown render as plain links with
|
||||
no preview image.
|
||||
|
||||
---
|
||||
|
||||
## Upgrading Forgejo
|
||||
|
||||
After upgrading Forgejo upstream:
|
||||
|
||||
1. Re-apply `go.patch` (or apply the two hunks by hand if context has shifted).
|
||||
2. Copy `ifc_url.go` and `ifc_url_test.go` back in — they are not modified by
|
||||
upstream.
|
||||
3. Run the Go tests to verify compatibility.
|
||||
4. Rebuild and redeploy.
|
||||
|
||||
Custom assets and `footer.tmpl` do not require a rebuild and are unaffected by
|
||||
Forgejo upgrades.
|
||||
31
forgejo/go.patch
Normal file
31
forgejo/go.patch
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
|
||||
index 2b19e0f1c9..12be3e1c24 100644
|
||||
--- a/modules/markup/markdown/markdown.go
|
||||
+++ b/modules/markup/markdown/markdown.go
|
||||
@@ -116,6 +116,7 @@ func SpecializedMarkdown() goldmark.Markdown {
|
||||
math.NewExtension(
|
||||
math.Enabled(setting.Markdown.EnableMath),
|
||||
),
|
||||
+ NewIfcURLExtension(),
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAttribute(),
|
||||
diff --git a/modules/setting/markup.go b/modules/setting/markup.go
|
||||
index 4ab9e7b2d1..c1fad254b4 100644
|
||||
--- a/modules/setting/markup.go
|
||||
+++ b/modules/setting/markup.go
|
||||
@@ -59,8 +59,14 @@ type MarkupSanitizerRule struct {
|
||||
AllowDataURIImages bool
|
||||
}
|
||||
|
||||
+// IfcURL holds configuration for the ifcurl preview service integration.
|
||||
+var IfcURL = struct {
|
||||
+ PreviewServiceURL string `ini:"PREVIEW_SERVICE_URL"`
|
||||
+}{}
|
||||
+
|
||||
func loadMarkupFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "markdown", &Markdown)
|
||||
+ mustMapSetting(rootCfg, "ifcurl", &IfcURL)
|
||||
|
||||
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(50000)
|
||||
FilePreviewMaxLines = rootCfg.Section("markup").Key("FILEPREVIEW_MAX_LINES").MustInt(50)
|
||||
269
forgejo/modules/markup/markdown/ifc_url.go
Normal file
269
forgejo/modules/markup/markdown/ifc_url.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// ifc_url.go adds preview rendering for ifc:// URLs in Markdown.
|
||||
//
|
||||
// Configuration (app.ini):
|
||||
//
|
||||
// [ifcurl]
|
||||
// PREVIEW_SERVICE_URL = http://localhost:8000
|
||||
//
|
||||
// Integration in markdown.go — add to goldmark.WithExtensions(...):
|
||||
//
|
||||
// NewIfcURLExtension(),
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var ifcURLKind = ast.NewNodeKind("IfcURL")
|
||||
|
||||
type ifcURLNode struct {
|
||||
ast.BaseInline
|
||||
rawURL string
|
||||
}
|
||||
|
||||
func (n *ifcURLNode) Kind() ast.NodeKind { return ifcURLKind }
|
||||
func (n *ifcURLNode) Dump(src []byte, level int) {
|
||||
ast.DumpHelper(n, src, level, map[string]string{"URL": n.rawURL}, nil)
|
||||
}
|
||||
|
||||
// ifcURLTransformer walks the parsed AST and replaces ifc:// occurrences with
|
||||
// ifcURLNode. It handles both [title](ifc://...) links and bare ifc:// URLs.
|
||||
//
|
||||
// Bare URLs may span multiple consecutive text-like siblings because goldmark's
|
||||
// emphasis parser splits text nodes at delimiter characters (e.g. the '_' in
|
||||
// "?path=_test.ifc"), so we collect siblings until a URL terminator is found.
|
||||
type ifcURLTransformer struct{}
|
||||
|
||||
// urlTerminators end a bare ifc:// URL in inline text.
|
||||
const urlTerminators = " \t\n\r<>\"'"
|
||||
|
||||
func (t *ifcURLTransformer) Transform(doc *ast.Document, reader text.Reader, _ parser.Context) {
|
||||
if setting.IfcURL.PreviewServiceURL == "" {
|
||||
return
|
||||
}
|
||||
src := reader.Source()
|
||||
|
||||
// Replace [title](ifc://...) link nodes.
|
||||
var links []*ast.Link
|
||||
if err := ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
if link, ok := n.(*ast.Link); ok && strings.HasPrefix(string(link.Destination), "ifc://") {
|
||||
links = append(links, link)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
for _, link := range links {
|
||||
link.Parent().ReplaceChild(link.Parent(), link, &ifcURLNode{rawURL: string(link.Destination)})
|
||||
}
|
||||
|
||||
// Replace bare ifc:// URLs in inline text nodes.
|
||||
type span struct {
|
||||
parent ast.Node
|
||||
first ast.Node
|
||||
spanned []ast.Node // siblings consumed beyond first (last element is the final node)
|
||||
rawURL string
|
||||
before []byte // text in first node before "ifc://"
|
||||
trailing []byte // text in last node after the URL end
|
||||
}
|
||||
|
||||
var spans []span
|
||||
claimed := map[ast.Node]bool{}
|
||||
|
||||
if err := ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering || claimed[n] {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
tb, ok := inlineText(n, src)
|
||||
if !ok {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
idx := bytes.Index(tb, []byte("ifc://"))
|
||||
if idx < 0 {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
before := tb[:idx]
|
||||
urlBytes := append([]byte(nil), tb[idx:]...)
|
||||
var spanned []ast.Node
|
||||
var trailing []byte
|
||||
|
||||
if end := bytes.IndexAny(urlBytes, urlTerminators); end >= 0 {
|
||||
trailing = append([]byte(nil), urlBytes[end:]...)
|
||||
urlBytes = urlBytes[:end]
|
||||
} else {
|
||||
for sib := n.NextSibling(); sib != nil; sib = sib.NextSibling() {
|
||||
sibTB, ok := inlineText(sib, src)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
spanned = append(spanned, sib)
|
||||
if end := bytes.IndexAny(sibTB, urlTerminators); end >= 0 {
|
||||
urlBytes = append(urlBytes, sibTB[:end]...)
|
||||
trailing = append([]byte(nil), sibTB[end:]...)
|
||||
break
|
||||
}
|
||||
urlBytes = append(urlBytes, sibTB...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(urlBytes) <= len("ifc://") {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
spans = append(spans, span{
|
||||
parent: n.Parent(),
|
||||
first: n,
|
||||
spanned: spanned,
|
||||
rawURL: string(urlBytes),
|
||||
before: append([]byte(nil), before...),
|
||||
trailing: trailing,
|
||||
})
|
||||
claimed[n] = true
|
||||
for _, s := range spanned {
|
||||
claimed[s] = true
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range spans {
|
||||
parent := s.parent
|
||||
if parent == nil {
|
||||
continue
|
||||
}
|
||||
last := s.first
|
||||
if len(s.spanned) > 0 {
|
||||
last = s.spanned[len(s.spanned)-1]
|
||||
}
|
||||
|
||||
// Remove intermediate spanned nodes; replace or remove the last one.
|
||||
for _, m := range s.spanned {
|
||||
if m == last {
|
||||
continue
|
||||
}
|
||||
parent.RemoveChild(parent, m)
|
||||
}
|
||||
if last != s.first {
|
||||
if len(s.trailing) > 0 {
|
||||
parent.ReplaceChild(parent, last, ast.NewString(s.trailing))
|
||||
} else {
|
||||
parent.RemoveChild(parent, last)
|
||||
}
|
||||
}
|
||||
|
||||
// Build replacement nodes for first: [before?] ifcURLNode [trailing if single-node?]
|
||||
ifcNode := &ifcURLNode{rawURL: s.rawURL}
|
||||
seq := make([]ast.Node, 0, 3)
|
||||
if len(s.before) > 0 {
|
||||
seq = append(seq, ast.NewString(s.before))
|
||||
}
|
||||
seq = append(seq, ifcNode)
|
||||
if last == s.first && len(s.trailing) > 0 {
|
||||
seq = append(seq, ast.NewString(s.trailing))
|
||||
}
|
||||
|
||||
parent.ReplaceChild(parent, s.first, seq[0])
|
||||
prev := seq[0]
|
||||
for _, nn := range seq[1:] {
|
||||
parent.InsertAfter(parent, prev, nn)
|
||||
prev = nn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inlineText returns the raw bytes of an ast.Text or ast.String node.
|
||||
func inlineText(n ast.Node, src []byte) ([]byte, bool) {
|
||||
switch s := n.(type) {
|
||||
case *ast.Text:
|
||||
seg := s.Segment
|
||||
return src[seg.Start:seg.Stop], true
|
||||
case *ast.String:
|
||||
return s.Value, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type ifcURLRenderer struct {
|
||||
previewServiceURL string
|
||||
}
|
||||
|
||||
func (r *ifcURLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ifcURLKind, r.render)
|
||||
}
|
||||
|
||||
func (r *ifcURLRenderer) render(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
n := node.(*ifcURLNode)
|
||||
|
||||
previewURL := r.previewServiceURL + "/preview?url=" + url.QueryEscape(n.rawURL)
|
||||
viewerURL := "/assets/viewer.html?url=" + url.QueryEscape(n.rawURL)
|
||||
safeIfc := htmlEscapeString(n.rawURL)
|
||||
safePreview := htmlEscapeString(previewURL)
|
||||
safeViewer := htmlEscapeString(viewerURL)
|
||||
|
||||
fmt.Fprintf(w,
|
||||
`<figure class="ifcurl-preview">`+
|
||||
`<a href="%s" title="%s">`+
|
||||
`<img src="%s" alt="IFC preview" loading="lazy" style="max-width:100%%"/>`+
|
||||
`</a>`+
|
||||
`<figcaption><code>%s</code></figcaption>`+
|
||||
`</figure>`,
|
||||
safeViewer, safeIfc, safePreview, safeIfc,
|
||||
)
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
func htmlEscapeString(s string) string {
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, `"`, """)
|
||||
s = strings.ReplaceAll(s, "'", "'")
|
||||
return s
|
||||
}
|
||||
|
||||
type ifcURLExtension struct {
|
||||
previewServiceURL string
|
||||
}
|
||||
|
||||
func NewIfcURLExtension() goldmark.Extender {
|
||||
return &ifcURLExtension{
|
||||
previewServiceURL: setting.IfcURL.PreviewServiceURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ifcURLExtension) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(&ifcURLTransformer{}, 999),
|
||||
),
|
||||
)
|
||||
m.Renderer().AddOptions(
|
||||
renderer.WithNodeRenderers(
|
||||
util.Prioritized(&ifcURLRenderer{previewServiceURL: e.previewServiceURL}, 999),
|
||||
),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue