first-commit
This commit is contained in:
20
web_src/js/render/ansi.test.ts
Normal file
20
web_src/js/render/ansi.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {renderAnsi} from './ansi.ts';
|
||||
|
||||
test('renderAnsi', () => {
|
||||
expect(renderAnsi('abc')).toEqual('abc');
|
||||
expect(renderAnsi('abc\n')).toEqual('abc');
|
||||
expect(renderAnsi('abc\r\n')).toEqual('abc');
|
||||
expect(renderAnsi('\r')).toEqual('');
|
||||
expect(renderAnsi('\rx\rabc')).toEqual('x\nabc');
|
||||
expect(renderAnsi('\rabc\rx\r')).toEqual('abc\nx');
|
||||
expect(renderAnsi('\x1b[30mblack\x1b[37mwhite')).toEqual('<span class="ansi-black-fg">black</span><span class="ansi-white-fg">white</span>'); // unclosed
|
||||
expect(renderAnsi('<script>')).toEqual('<script>');
|
||||
expect(renderAnsi('\x1b[1A\x1b[2Ktest\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
|
||||
expect(renderAnsi('\x1b[1A\x1b[2K\rtest\r\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
|
||||
expect(renderAnsi('\x1b[1A\x1b[2Ktest\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
|
||||
expect(renderAnsi('\x1b[1A\x1b[2K\rtest\r\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
|
||||
|
||||
// treat "\033[0K" and "\033[0J" (Erase display/line) as "\r", then it will be covered to "\n" finally.
|
||||
expect(renderAnsi('a\x1b[Kb\x1b[2Jc')).toEqual('a\nb\nc');
|
||||
expect(renderAnsi('\x1b[48;5;88ma\x1b[38;208;48;5;159mb\x1b[m')).toEqual(`<span style="background-color:rgb(135,0,0)">a</span><span style="background-color:rgb(175,255,255)">b</span>`);
|
||||
});
|
45
web_src/js/render/ansi.ts
Normal file
45
web_src/js/render/ansi.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {AnsiUp} from 'ansi_up';
|
||||
|
||||
const replacements: Array<[RegExp, string]> = [
|
||||
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
|
||||
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
|
||||
];
|
||||
|
||||
// render ANSI to HTML
|
||||
export function renderAnsi(line: string): string {
|
||||
// create a fresh ansi_up instance because otherwise previous renders can influence
|
||||
// the output of future renders, because ansi_up is stateful and remembers things like
|
||||
// unclosed opening tags for colors.
|
||||
const ansi_up = new AnsiUp();
|
||||
ansi_up.use_classes = true;
|
||||
|
||||
if (line.endsWith('\r\n')) {
|
||||
line = line.substring(0, line.length - 2);
|
||||
} else if (line.endsWith('\n')) {
|
||||
line = line.substring(0, line.length - 1);
|
||||
}
|
||||
|
||||
if (line.includes('\x1b')) {
|
||||
for (const [regex, replacement] of replacements) {
|
||||
line = line.replace(regex, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
if (!line.includes('\r')) {
|
||||
return ansi_up.ansi_to_html(line);
|
||||
}
|
||||
|
||||
// handle "\rReading...1%\rReading...5%\rReading...100%",
|
||||
// convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
|
||||
const lines = [];
|
||||
for (const part of line.split('\r')) {
|
||||
if (part === '') continue;
|
||||
const partHtml = ansi_up.ansi_to_html(part);
|
||||
if (partHtml !== '') {
|
||||
lines.push(partHtml);
|
||||
}
|
||||
}
|
||||
|
||||
// the log message element is with "white-space: break-spaces;", so use "\n" to break lines
|
||||
return lines.join('\n');
|
||||
}
|
10
web_src/js/render/plugin.ts
Normal file
10
web_src/js/render/plugin.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type FileRenderPlugin = {
|
||||
// unique plugin name
|
||||
name: string;
|
||||
|
||||
// test if plugin can handle a specified file
|
||||
canHandle: (filename: string, mimeType: string) => boolean;
|
||||
|
||||
// render file content
|
||||
render: (container: HTMLElement, fileUrl: string, options?: any) => Promise<void>;
|
||||
}
|
60
web_src/js/render/plugins/3d-viewer.ts
Normal file
60
web_src/js/render/plugins/3d-viewer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type {FileRenderPlugin} from '../plugin.ts';
|
||||
import {extname} from '../../utils.ts';
|
||||
|
||||
// support common 3D model file formats, use online-3d-viewer library for rendering
|
||||
|
||||
// eslint-disable-next-line multiline-comment-style
|
||||
/* 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(/* webpackChunkName: "online-3d-viewer" */'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]);
|
||||
},
|
||||
};
|
||||
}
|
20
web_src/js/render/plugins/pdf-viewer.ts
Normal file
20
web_src/js/render/plugins/pdf-viewer.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type {FileRenderPlugin} from '../plugin.ts';
|
||||
|
||||
export function newRenderPluginPdfViewer(): FileRenderPlugin {
|
||||
return {
|
||||
name: 'pdf-viewer',
|
||||
|
||||
canHandle(filename: string, _mimeType: string): boolean {
|
||||
return filename.toLowerCase().endsWith('.pdf');
|
||||
},
|
||||
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
const PDFObject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');
|
||||
// TODO: the PDFObject library does not support dynamic height adjustment,
|
||||
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