mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-23 03:00:14 +08:00
Frontend iframe renderer framework: 3D models, OpenAPI (#37233)
Introduces a frontend external-render framework that runs renderer plugins inside an `iframe` (loaded via `srcdoc` to keep the CSP `sandbox` directive working without origin-related console noise), and migrates the 3D viewer and OpenAPI/Swagger renderers onto it. PDF and asciicast paths are refactored to share the same `data-render-name` mechanism. Adds e2e coverage for 3D, PDF, asciicast and OpenAPI render paths, plus a regression for the `RefTypeNameSubURL` double-escape on non-ASCII branch names. Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
import type {FileRenderPlugin} from '../plugin.ts';
|
||||
import {extname} from '../../utils.ts';
|
||||
|
||||
// support common 3D model file formats, use online-3d-viewer library for rendering
|
||||
|
||||
/* a simple text STL file example:
|
||||
solid SimpleTriangle
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex 0 0 0
|
||||
vertex 1 0 0
|
||||
vertex 0 1 0
|
||||
endloop
|
||||
endfacet
|
||||
endsolid SimpleTriangle
|
||||
*/
|
||||
|
||||
export function newRenderPlugin3DViewer(): FileRenderPlugin {
|
||||
// Some extensions are text-based formats:
|
||||
// .3mf .amf .brep: XML
|
||||
// .fbx: XML or BINARY
|
||||
// .dae .gltf: JSON
|
||||
// .ifc, .igs, .iges, .stp, .step are: TEXT
|
||||
// .stl .ply: TEXT or BINARY
|
||||
// .obj .off .wrl: TEXT
|
||||
// So we need to be able to render when the file is recognized as plaintext file by backend.
|
||||
//
|
||||
// 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: "*.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.
|
||||
const SUPPORTED_EXTENSIONS = [
|
||||
'.3dm', '.3ds', '.3mf', '.amf', '.bim', '.brep',
|
||||
'.dae', '.fbx', '.fcstd', '.glb', '.gltf',
|
||||
'.ifc', '.igs', '.iges', '.stp', '.step',
|
||||
'.stl', '.obj', '.off', '.ply', '.wrl',
|
||||
];
|
||||
|
||||
return {
|
||||
name: '3d-model-viewer',
|
||||
|
||||
canHandle(filename: string, _mimeType: string): boolean {
|
||||
const ext = extname(filename).toLowerCase();
|
||||
return SUPPORTED_EXTENSIONS.includes(ext);
|
||||
},
|
||||
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
// TODO: height and/or max-height?
|
||||
const OV = await import('online-3d-viewer');
|
||||
const viewer = new OV.EmbeddedViewer(container, {
|
||||
backgroundColor: new OV.RGBAColor(59, 68, 76, 0),
|
||||
defaultColor: new OV.RGBColor(65, 131, 196),
|
||||
edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1),
|
||||
});
|
||||
viewer.LoadModelFromUrlList([fileUrl]);
|
||||
},
|
||||
};
|
||||
}
|
||||
17
web_src/js/render/plugins/frontend-openapi-swagger.ts
Normal file
17
web_src/js/render/plugins/frontend-openapi-swagger.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type {FrontendRenderFunc} from '../plugin.ts';
|
||||
import {initSwaggerUI} from '../swagger.ts';
|
||||
|
||||
// HINT: SWAGGER-CSS-IMPORT: this import is also necessary when swagger is used as a frontend external render
|
||||
// It must be on top-level, doesn't work in a function
|
||||
// Static import doesn't work (it needs to use manifest.json to manually add the CSS file)
|
||||
await import('../../../css/swagger.css');
|
||||
|
||||
export const frontendRender: FrontendRenderFunc = async (opts): Promise<boolean> => {
|
||||
try {
|
||||
await initSwaggerUI(opts.container, {specText: opts.contentString()});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
36
web_src/js/render/plugins/frontend-viewer-3d.ts
Normal file
36
web_src/js/render/plugins/frontend-viewer-3d.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type {FrontendRenderFunc} from '../plugin.ts';
|
||||
import {basename} from '../../utils.ts';
|
||||
import * as OV from 'online-3d-viewer';
|
||||
import {colord} from 'colord';
|
||||
|
||||
/* a simple text STL file example:
|
||||
solid SimpleTriangle
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex 0 0 0
|
||||
vertex 1 0 0
|
||||
vertex 0 1 0
|
||||
endloop
|
||||
endfacet
|
||||
endsolid SimpleTriangle
|
||||
*/
|
||||
|
||||
export const frontendRender: FrontendRenderFunc = async (opts): Promise<boolean> => {
|
||||
try {
|
||||
opts.container.style.height = `${window.innerHeight}px`;
|
||||
const bgColor = colord(getComputedStyle(document.body).backgroundColor).toRgb();
|
||||
const primaryColor = colord(getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim()).toRgb();
|
||||
const viewer = new OV.EmbeddedViewer(opts.container, {
|
||||
backgroundColor: new OV.RGBAColor(bgColor.r, bgColor.g, bgColor.b, 255),
|
||||
defaultColor: new OV.RGBColor(primaryColor.r, primaryColor.g, primaryColor.b),
|
||||
edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1),
|
||||
});
|
||||
const blob = new Blob([opts.contentBytes()]);
|
||||
const file = new File([blob], basename(opts.treePath));
|
||||
viewer.LoadModelFromFileList([file]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {FileRenderPlugin} from '../plugin.ts';
|
||||
import type {InplaceRenderPlugin} from '../plugin.ts';
|
||||
|
||||
export function newRenderPluginPdfViewer(): FileRenderPlugin {
|
||||
export function newInplacePluginPdfViewer(): InplaceRenderPlugin {
|
||||
return {
|
||||
name: 'pdf-viewer',
|
||||
|
||||
@@ -11,6 +11,7 @@ export function newRenderPluginPdfViewer(): FileRenderPlugin {
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
const PDFObject = await import('pdfobject');
|
||||
// TODO: the PDFObject library does not support dynamic height adjustment,
|
||||
// TODO: it seems that this render must be an inplace render, because the URL must be accessible from the current context
|
||||
container.style.height = `${window.innerHeight - 100}px`;
|
||||
if (!PDFObject.default.embed(fileUrl, container)) {
|
||||
throw new Error('Unable to render the PDF file');
|
||||
Reference in New Issue
Block a user