Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
_ "code.gitea.io/gitea/modules/markup/console"
_ "code.gitea.io/gitea/modules/markup/csv"
_ "code.gitea.io/gitea/modules/markup/markdown"
_ "code.gitea.io/gitea/modules/markup/openapi"
_ "code.gitea.io/gitea/modules/markup/orgmode"

"github.com/urfave/cli/v2"
Expand Down
90 changes: 90 additions & 0 deletions modules/markup/openapi/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package openapi

import (
"fmt"
"io"
"net/url"

"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"

"github.com/gobwas/glob"
)

func init() {
markup.RegisterRenderer(Renderer{})
}

// Renderer implements markup.Renderer for openapi files.
type Renderer struct{}

var (
_ markup.RendererRelativePathDetector = (*Renderer)(nil)
g = glob.MustCompile("**{openapi,OpenAPI,swagger}.{yml,yaml,json,JSON,Yaml,YML}", '/')
)

// Name implements markup.Renderer
func (Renderer) Name() string {
return "openapi"
}

// SanitizerDisabled disabled sanitize if return true
func (Renderer) SanitizerDisabled() bool {
return true
}

func (Renderer) DisplayInNewPage() bool {
return true
}

func (Renderer) CanRenderRelativePath(relativePath string) bool {
return g.Match(relativePath)
}

// Extensions implements markup.Renderer
func (Renderer) Extensions() []string {
return nil
}

// SanitizerRules implements markup.Renderer
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{
{Element: "script", AllowAttr: "src"},
}
}

// Render implements markup.Renderer
func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error {
rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s",
setting.AppSubURL,
url.PathEscape(ctx.Metas["user"]),
url.PathEscape(ctx.Metas["repo"]),
ctx.Metas["BranchNameSubURL"],
ctx.RelativePath,
)

if _, err := io.WriteString(output, fmt.Sprintf(
`<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="%s/assets/css/swagger.css?v=%s">
</head>
<body>
<div id="swagger-ui" data-source="%s"></div>
<script src="%s/assets/js/swagger.js?v=%s"></script>
</body>
</html>`,
setting.StaticURLPrefix,
setting.AssetVersion,
rawURL,
setting.StaticURLPrefix,
setting.AssetVersion,
)); err != nil {
return err
}
return nil
}
87 changes: 66 additions & 21 deletions modules/markup/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,34 @@ type PostProcessRenderer interface {
NeedPostProcess() bool
}

// PostProcessRenderer defines an interface for external renderers
type ExternalRenderer interface {
type SanitizerDisabledRenderer interface {
// SanitizerDisabled disabled sanitize if return true
SanitizerDisabled() bool
}

// PostProcessRenderer defines an interface for external renderers
type ExternalRenderer interface {
SanitizerDisabledRenderer

// DisplayInIFrame represents whether render the content with an iframe
DisplayInIFrame() bool
}

type NewPageRenderer interface {
DisplayInNewPage() bool
}

// RendererContentDetector detects if the content can be rendered
// by specified renderer
type RendererContentDetector interface {
CanRender(filename string, input io.Reader) bool
}

// RendererRelativePathDetector detects if the content can be rendered according relative file path
type RendererRelativePathDetector interface {
CanRenderRelativePath(relativePath string) bool
}

var (
extRenderers = make(map[string]Renderer)
renderers = make(map[string]Renderer)
Expand All @@ -203,7 +216,21 @@ func RegisterRenderer(renderer Renderer) {
// GetRendererByFileName get renderer by filename
func GetRendererByFileName(filename string) Renderer {
extension := strings.ToLower(filepath.Ext(filename))
return extRenderers[extension]
renderer := extRenderers[extension]
if renderer != nil {
return renderer
}
return GetRendererByRelativePathInterface(filename)
}

// GetRendererByRelativePathInterface returns a renderer according relative file path
func GetRendererByRelativePathInterface(relativePath string) Renderer {
for _, renderer := range renderers {
if detector, ok := renderer.(RendererRelativePathDetector); ok && detector.CanRenderRelativePath(relativePath) {
return renderer
}
}
return nil
}

// GetRendererByType returns a renderer according type
Expand Down Expand Up @@ -284,7 +311,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
var pw2 io.WriteCloser

var sanitizerDisabled bool
if r, ok := renderer.(ExternalRenderer); ok {
if r, ok := renderer.(SanitizerDisabledRenderer); ok {
sanitizerDisabled = r.SanitizerDisabled()
}

Expand Down Expand Up @@ -342,33 +369,51 @@ func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error {
return ErrUnsupportedRenderType{ctx.Type}
}

// ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
type ErrUnsupportedRenderExtension struct {
Extension string
// ErrUnsupportedRenderFile represents the error when extension or filename doesn't supported to render
type ErrUnsupportedRenderFile struct {
RelativePath string
}

func IsErrUnsupportedRenderExtension(err error) bool {
_, ok := err.(ErrUnsupportedRenderExtension)
func IsErrUnsupportedRenderFile(err error) bool {
_, ok := err.(ErrUnsupportedRenderFile)
return ok
}

func (err ErrUnsupportedRenderExtension) Error() string {
return fmt.Sprintf("Unsupported render extension: %s", err.Extension)
func (err ErrUnsupportedRenderFile) Error() string {
return fmt.Sprintf("Unsupported render file: %s", err.RelativePath)
}

func renderButton(ctx *RenderContext, output io.Writer) error {
_, err := io.WriteString(output, fmt.Sprintf(`<iframe src="%s/%s/%s/render/%s/%s" sandbox="allow-same-origin allow-scripts"></iframe>`,
setting.AppSubURL,
url.PathEscape(ctx.Metas["user"]),
url.PathEscape(ctx.Metas["repo"]),
ctx.Metas["BranchNameSubURL"],
url.PathEscape(ctx.RelativePath),
))
return err
}

func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
if renderer, ok := extRenderers[extension]; ok {
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
if !ctx.InStandalonePage {
// for an external render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame(ctx, output)
}
renderer := GetRendererByFileName(ctx.RelativePath)
if renderer == nil {
return ErrUnsupportedRenderFile{ctx.RelativePath}
}

if r, ok := renderer.(NewPageRenderer); ok && r.DisplayInNewPage() {
if !ctx.InStandalonePage {
return renderButton(ctx, output)
}
}

if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
if !ctx.InStandalonePage {
// for an external render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame(ctx, output)
}
return render(ctx, renderer, input, output)
}
return ErrUnsupportedRenderExtension{extension}
return render(ctx, renderer, input, output)
}

// Type returns if markup format via the filename
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"htmx.org": "1.9.11",
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"js-yaml": "4.1.0",
"katex": "0.16.10",
"license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.9.0",
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/misc/markup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Here are some links to the most important topics. You can find the full list of
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
}

testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render file: path/test.unknown\n", http.StatusUnprocessableEntity)
testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
}

Expand Down
3 changes: 1 addition & 2 deletions routers/common/markup.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,10 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
Type: markupType,
RelativePath: relativePath,
}, strings.NewReader(text), ctx.Resp); err != nil {
if markup.IsErrUnsupportedRenderExtension(err) {
if markup.IsErrUnsupportedRenderFile(err) {
ctx.Error(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.Error(http.StatusInternalServerError, err.Error())
}
return
}
}
7 changes: 5 additions & 2 deletions routers/web/repo/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func RenderFile(ctx *context.Context) {
isTextFile := st.IsText()

rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts allow-same-origin")

if markupType := markup.Type(blob.Name()); markupType == "" {
if isTextFile {
Expand All @@ -56,6 +56,9 @@ func RenderFile(ctx *context.Context) {
return
}

metaData := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
metaData["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()

err = markup.Render(&markup.RenderContext{
Ctx: ctx,
RelativePath: ctx.Repo.TreePath,
Expand All @@ -64,7 +67,7 @@ func RenderFile(ctx *context.Context) {
BranchPath: ctx.Repo.BranchNameSubURL(),
TreePath: path.Dir(ctx.Repo.TreePath),
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
Metas: metaData,
GitRepo: ctx.Repo.GitRepo,
InStandalonePage: true,
}, rd, ctx.Resp)
Expand Down
1 change: 1 addition & 0 deletions web_src/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
--height-loading: 16rem;
--min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
--tab-size: 4;
--render-height: 600px;
--checkbox-size: 15px; /* height and width of checkbox and radio inputs */
--page-spacing: 16px; /* space between page elements */
--page-margin-x: 32px; /* minimum space on left and right side of page */
Expand Down
13 changes: 12 additions & 1 deletion web_src/css/repo.css
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ td .commit-summary {

.pdf-content {
width: 100%;
height: 600px;
height: var(--render-height);
border: none !important;
display: flex;
align-items: center;
Expand Down Expand Up @@ -1788,6 +1788,17 @@ td .commit-summary {
.file-view.markup {
padding: 1em 2em;
}

.file-view.openapi {
padding: 0;
}

.file-view.openapi iframe {
width: 100%;
height: var(--render-height);
border: none;
}

.repository .activity-header {
display: flex;
justify-content: space-between;
Expand Down
26 changes: 16 additions & 10 deletions web_src/js/standalone/swagger.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js';
import 'swagger-ui-dist/swagger-ui.css';
import {parseUrl} from '../utils.js';
import {load} from 'js-yaml';

// This code is shared for our own spec as well as user-defined specs via files in repo
window.addEventListener('load', async () => {
const url = document.getElementById('swagger-ui').getAttribute('data-source');
const url = parseUrl(document.getElementById('swagger-ui').getAttribute('data-source'));
const res = await fetch(url);
const spec = await res.json();
const text = await res.text();
const spec = /\.ya?ml$/i.test(url.pathname) ? load(text) : JSON.parse(text);
const isOwnSpec = url.pathname.endsWith('/swagger.v1.json');

// Make the page's protocol be at the top of the schemes list
const proto = window.location.protocol.slice(0, -1);
spec.schemes.sort((a, b) => {
if (a === proto) return -1;
if (b === proto) return 1;
return 0;
});
if (isOwnSpec) {
// Make the page's protocol be at the top of the schemes list
const proto = window.location.protocol.slice(0, -1);
spec.schemes.sort((a, b) => {
if (a === proto) return -1;
if (b === proto) return 1;
return 0;
});
}

const ui = SwaggerUI({
spec,
dom_id: '#swagger-ui',
deepLinking: true,
docExpansion: 'none',
defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
presets: [
SwaggerUI.presets.apis,
],
Expand Down