mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
158 lines
4.5 KiB
Go
158 lines
4.5 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package external
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/markup"
|
|
"github.com/hanzoai/git/modules/process"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
|
|
"github.com/kballard/go-shellquote"
|
|
)
|
|
|
|
// RegisterRenderers registers all supported third part renderers according settings
|
|
func RegisterRenderers() {
|
|
markup.RegisterRenderer(&frontendRenderer{
|
|
name: "openapi-swagger",
|
|
patterns: []string{
|
|
"openapi.yaml",
|
|
"openapi.yml",
|
|
"openapi.json",
|
|
"swagger.yaml",
|
|
"swagger.yml",
|
|
"swagger.json",
|
|
},
|
|
})
|
|
|
|
markup.RegisterRenderer(&frontendRenderer{
|
|
name: "viewer-3d",
|
|
patterns: []string{
|
|
// It needs more logic to make it overall right (render a text 3D model automatically):
|
|
// we need to distinguish the ambiguous filename extensions.
|
|
// For example: "*.amf, *.obj, *.off, *.step" might be or not be a 3D model file.
|
|
// So when it is a text file, we can't assume that "we only render it by 3D plugin",
|
|
// otherwise the end users would be impossible to view its real content when the file is not a 3D model.
|
|
"*.3dm", "*.3ds", "*.3mf", "*.amf", "*.bim", "*.brep",
|
|
"*.dae", "*.fbx", "*.fcstd", "*.glb", "*.gltf",
|
|
"*.ifc", "*.igs", "*.iges", "*.stp", "*.step",
|
|
"*.stl", "*.obj", "*.off", "*.ply", "*.wrl",
|
|
},
|
|
})
|
|
|
|
markup.RegisterRenderer(&frontendRenderer{
|
|
name: "asciicast",
|
|
patterns: []string{"*.cast"},
|
|
})
|
|
|
|
for _, renderer := range setting.ExternalMarkupRenderers {
|
|
markup.RegisterRenderer(&Renderer{renderer})
|
|
}
|
|
}
|
|
|
|
// Renderer implements markup.Renderer for external tools
|
|
type Renderer struct {
|
|
*setting.MarkupRenderer
|
|
}
|
|
|
|
var (
|
|
_ markup.PostProcessRenderer = (*Renderer)(nil)
|
|
_ markup.ExternalRenderer = (*Renderer)(nil)
|
|
)
|
|
|
|
func (p *Renderer) Name() string {
|
|
return p.MarkupName
|
|
}
|
|
|
|
func (p *Renderer) NeedPostProcess() bool {
|
|
return p.MarkupRenderer.NeedPostProcess
|
|
}
|
|
|
|
func (p *Renderer) FileNamePatterns() []string {
|
|
return p.FilePatterns
|
|
}
|
|
|
|
func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|
return p.MarkupSanitizerRules
|
|
}
|
|
|
|
func (p *Renderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
|
|
ret.SanitizerDisabled = p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
|
|
ret.DisplayInIframe = p.RenderContentMode == setting.RenderContentModeIframe
|
|
ret.ContentSandbox = p.RenderContentSandbox
|
|
return ret
|
|
}
|
|
|
|
func envMark(envName string) string {
|
|
if runtime.GOOS == "windows" {
|
|
return "%" + envName + "%"
|
|
}
|
|
return "$" + envName
|
|
}
|
|
|
|
// Render renders the data of the document to HTML via the external tool.
|
|
func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
|
baseLinkSrc := ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault)
|
|
baseLinkRaw := ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw)
|
|
command := strings.NewReplacer(
|
|
envMark("GIT_PREFIX_SRC"), baseLinkSrc,
|
|
envMark("GIT_PREFIX_RAW"), baseLinkRaw,
|
|
).Replace(p.Command)
|
|
commands, err := shellquote.Split(command)
|
|
if err != nil || len(commands) == 0 {
|
|
return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err)
|
|
}
|
|
args := commands[1:]
|
|
|
|
if p.IsInputFile {
|
|
// write to temp file
|
|
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("forge_input")
|
|
if err != nil {
|
|
return fmt.Errorf("%s create temp file when rendering %s failed: %w", p.Name(), p.Command, err)
|
|
}
|
|
defer cleanup()
|
|
|
|
_, err = io.Copy(f, input)
|
|
if err != nil {
|
|
_ = f.Close()
|
|
return fmt.Errorf("%s write data to temp file when rendering %s failed: %w", p.Name(), p.Command, err)
|
|
}
|
|
|
|
err = f.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("%s close temp file when rendering %s failed: %w", p.Name(), p.Command, err)
|
|
}
|
|
args = append(args, f.Name())
|
|
}
|
|
|
|
processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], baseLinkSrc))
|
|
defer finished()
|
|
|
|
cmd := exec.CommandContext(processCtx, commands[0], args...)
|
|
cmd.Env = append(
|
|
os.Environ(),
|
|
"GIT_PREFIX_SRC="+baseLinkSrc,
|
|
"GIT_PREFIX_RAW="+baseLinkRaw,
|
|
)
|
|
if !p.IsInputFile {
|
|
cmd.Stdin = input
|
|
}
|
|
var stderr bytes.Buffer
|
|
cmd.Stdout = output
|
|
cmd.Stderr = &stderr
|
|
process.SetSysProcAttribute(cmd)
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("%s render run command %s %v failed: %w\nStderr: %s", p.Name(), commands[0], args, err, stderr.String())
|
|
}
|
|
return nil
|
|
}
|