mirror of https://github.com/portainer/portainer
				
				
				
			
		
			
				
	
	
		
			157 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			157 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
| import tokenize from '@nxmix/tokenize-ansi';
 | |
| import { FontWeight } from 'xterm';
 | |
| 
 | |
| import {
 | |
|   colors,
 | |
|   BACKGROUND_COLORS_BY_ANSI,
 | |
|   FOREGROUND_COLORS_BY_ANSI,
 | |
|   RGBColor,
 | |
| } from './colors';
 | |
| import { formatJSONLine } from './formatJSONLogs';
 | |
| import { formatZerologLogs, ZerologRegex } from './formatZerologLogs';
 | |
| import { Token, Span, TIMESTAMP_LENGTH, FormattedLine } from './types';
 | |
| 
 | |
| type FormatOptions = {
 | |
|   stripHeaders?: boolean;
 | |
|   withTimestamps?: boolean;
 | |
|   splitter?: string;
 | |
| };
 | |
| 
 | |
| const defaultOptions: FormatOptions = {
 | |
|   splitter: '\n',
 | |
| };
 | |
| 
 | |
| export function formatLogs(
 | |
|   rawLogs: string,
 | |
|   {
 | |
|     stripHeaders,
 | |
|     withTimestamps,
 | |
|     splitter = '\n',
 | |
|   }: FormatOptions = defaultOptions
 | |
| ) {
 | |
|   let logs = rawLogs;
 | |
|   if (stripHeaders) {
 | |
|     logs = stripHeadersFunc(logs);
 | |
|   }
 | |
|   // if JSON logs come serialized 2 times, parse them once to unwrap them
 | |
|   if (logs.startsWith('"')) {
 | |
|     try {
 | |
|       logs = JSON.parse(logs);
 | |
|     } catch (error) {
 | |
|       // noop, throw error away if logs cannot be parsed
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const tokens: Token[][] = tokenize(logs);
 | |
|   const formattedLogs: FormattedLine[] = [];
 | |
| 
 | |
|   let fgColor: string | undefined;
 | |
|   let bgColor: string | undefined;
 | |
|   let fontWeight: FontWeight | undefined;
 | |
|   let line = '';
 | |
|   let spans: Span[] = [];
 | |
| 
 | |
|   tokens.forEach((token) => {
 | |
|     const [type] = token;
 | |
| 
 | |
|     const fgAnsi = FOREGROUND_COLORS_BY_ANSI[type];
 | |
|     const bgAnsi = BACKGROUND_COLORS_BY_ANSI[type];
 | |
| 
 | |
|     if (fgAnsi) {
 | |
|       fgColor = cssColorFromRgb(fgAnsi);
 | |
|     } else if (type === 'moreColor') {
 | |
|       fgColor = extendedColorForToken(token);
 | |
|     } else if (type === 'fgDefault') {
 | |
|       fgColor = undefined;
 | |
|     } else if (bgAnsi) {
 | |
|       bgColor = cssColorFromRgb(bgAnsi);
 | |
|     } else if (type === 'bgMoreColor') {
 | |
|       bgColor = extendedColorForToken(token);
 | |
|     } else if (type === 'bgDefault') {
 | |
|       bgColor = undefined;
 | |
|     } else if (type === 'reset') {
 | |
|       fgColor = undefined;
 | |
|       bgColor = undefined;
 | |
|       fontWeight = undefined;
 | |
|     } else if (type === 'bold') {
 | |
|       fontWeight = 'bold';
 | |
|     } else if (type === 'normal') {
 | |
|       fontWeight = 'normal';
 | |
|     } else if (type === 'text') {
 | |
|       const tokenLines = (token[1] as string).split(splitter);
 | |
| 
 | |
|       tokenLines.forEach((tokenLine, idx) => {
 | |
|         if (idx && line) {
 | |
|           formattedLogs.push({ line, spans });
 | |
|           line = '';
 | |
|           spans = [];
 | |
|         }
 | |
| 
 | |
|         const text = stripEscapeCodes(tokenLine);
 | |
|         try {
 | |
|           if (
 | |
|             (!withTimestamps && text.startsWith('{')) ||
 | |
|             (withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))
 | |
|           ) {
 | |
|             const lines = formatJSONLine(text, withTimestamps);
 | |
|             formattedLogs.push(...lines);
 | |
|           } else if (
 | |
|             (!withTimestamps && ZerologRegex.test(text)) ||
 | |
|             (withTimestamps &&
 | |
|               ZerologRegex.test(text.substring(TIMESTAMP_LENGTH)))
 | |
|           ) {
 | |
|             const lines = formatZerologLogs(text, withTimestamps);
 | |
|             formattedLogs.push(...lines);
 | |
|           } else {
 | |
|             spans.push({ fgColor, bgColor, text, fontWeight });
 | |
|             line += text;
 | |
|           }
 | |
|         } catch (error) {
 | |
|           // in case parsing fails for whatever reason, push the raw logs and continue
 | |
|           spans.push({ fgColor, bgColor, text, fontWeight });
 | |
|           line += text;
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   });
 | |
|   if (line) {
 | |
|     formattedLogs.push({ line, spans });
 | |
|   }
 | |
| 
 | |
|   return formattedLogs;
 | |
| }
 | |
| 
 | |
| function stripHeadersFunc(logs: string) {
 | |
|   return logs.substring(8).replace(/\r?\n(.{8})/g, '\n');
 | |
| }
 | |
| 
 | |
| function stripEscapeCodes(logs: string) {
 | |
|   return logs.replace(
 | |
|     // eslint-disable-next-line no-control-regex
 | |
|     /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
 | |
|     ''
 | |
|   );
 | |
| }
 | |
| 
 | |
| function cssColorFromRgb(rgb: RGBColor) {
 | |
|   const [r, g, b] = rgb;
 | |
| 
 | |
|   return `rgb(${r}, ${g}, ${b})`;
 | |
| }
 | |
| 
 | |
| // assuming types based on original JS implementation
 | |
| // there is not much type definitions for the tokenize library
 | |
| function extendedColorForToken(token: Token[]) {
 | |
|   const [, colorMode, colorRef] = token as [undefined, number, number];
 | |
| 
 | |
|   if (colorMode === 2) {
 | |
|     return cssColorFromRgb(token.slice(2) as RGBColor);
 | |
|   }
 | |
| 
 | |
|   if (colorMode === 5 && colors[colorRef]) {
 | |
|     return cssColorFromRgb(colors[colorRef]);
 | |
|   }
 | |
| 
 | |
|   return '';
 | |
| }
 |