Files
teleport/web/packages/shared/components/Editor/Editor.tsx
T
Jeff Pihach 3f75acb163 Add license headers to web files that were missing it. (#21315)
* Lint the web files but not node_modules or vendored files.

* Add license headers to web files that were missing it.

* Add missing licenses.
2023-02-07 16:27:31 +00:00

324 lines
7.0 KiB
TypeScript

/**
* Copyright 2023 Gravitational, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import styled, { keyframes } from 'styled-components';
import {
WindowCode,
WindowContainer,
WindowContentContainer,
WindowTitleBar,
} from 'shared/components/Window';
import { Tabs } from 'shared/components/Editor/Tabs';
import { Language } from 'shared/components/Editor/Language';
import {
KeywordHighlight,
TerminalColor,
} from 'shared/components/AnimatedTerminal/TerminalContent';
import { File, FileProps } from './File';
interface EditorProps {
title: string;
}
export function Editor(props: React.PropsWithChildren<EditorProps>) {
const [activeTabIndex, setActiveTabIndex] = useState(0);
const files = React.Children.map(
props.children,
(child: React.ReactElement<FileProps>) => {
if (child.type === File) {
return {
name: child.props.name,
content: child.props.code,
language: child.props.language,
};
}
return null;
}
).filter(Boolean);
const tabs = files.map(file => file.name);
const { content, language } = files[activeTabIndex];
const parsed = parse(content, language);
const lineNumbers = [];
if (content) {
const numberOfLines = content.split('\n').length;
for (let i = 0; i <= numberOfLines; i++) {
lineNumbers.push(
<LineNumber
key={i}
data-line-number={i + 1}
active={i === numberOfLines}
/>
);
}
} else {
lineNumbers.push(<LineNumber key={0} data-line-number={1} active />);
}
return (
<WindowContainer>
<WindowTitleBar title={props.title} />
<Tabs
items={tabs}
activeIndex={activeTabIndex}
onSelect={setActiveTabIndex}
/>
<WindowContentContainer style={{ height: 585 }}>
<WindowCode style={{ display: 'flex' }}>
<LineNumbers>{lineNumbers}</LineNumbers>
<Code>
{parsed}
<ActiveLine>
<Cursor />
</ActiveLine>
</Code>
</WindowCode>
</WindowContentContainer>
</WindowContainer>
);
}
function parse(code: string, language: Language) {
switch (language) {
case Language.YAML:
return parseYAML(code);
default:
throw new Error('Language not supported');
}
}
function parseYAML(code: string) {
if (!code) {
return [];
}
const highlights = [
{
key: 'string',
keywords: [`'\\*'`],
color: TerminalColor.Keyword,
},
{
key: 'certificate',
match: /(-----.*?-----)/,
color: TerminalColor.Punctuation,
},
];
const lines = code.split('\n');
const result = [];
for (const [index, line] of lines.entries()) {
const highlightSemicolonMultiline = highlight(
line,
': |',
index,
highlights
);
if (highlightSemicolonMultiline) {
result.push(highlightSemicolonMultiline);
continue;
}
const highlightSemicolon = highlight(line, ':', index, highlights);
if (highlightSemicolon) {
result.push(highlightSemicolon);
continue;
}
if (line) {
result.push(<div key={index}>{highlightValue(line, highlights)}</div>);
continue;
}
result.push(<div key={index}>&nbsp;</div>);
}
return result;
}
function highlight(
code: string,
symbol: string,
index: number,
highlights: KeywordHighlight[]
) {
if (!code.includes(symbol)) {
return;
}
const symbolIndex = code.indexOf(symbol);
let value = code.substring(symbolIndex + symbol.length, code.length);
return (
<div key={index}>
<Keyword>{code.substring(0, symbolIndex)}</Keyword>
<Punctuation>{symbol}</Punctuation>
{highlightValue(value, highlights)}
</div>
);
}
function highlightValue(content: string, highlights: KeywordHighlight[]) {
for (const entry of highlights) {
if (entry.match && entry.match.test(content)) {
const split = content.split(entry.match);
return split
.map((item, index) => {
if (!item) {
return;
}
// all odd occurrences are matches, the rest remain unchanged
if (index % 2 === 0) {
return <span key={index}>{item}</span>;
}
return (
<span key={`${entry.key}-${index}`} style={{ color: entry.color }}>
{item}
</span>
);
})
.filter(Boolean);
}
}
const words = content.split(' ');
const result = [];
outer: for (const [index, word] of words.entries()) {
for (const entry of highlights) {
if (entry.keywords) {
const highlightedWords = highlightWord(word, entry);
if (highlightedWords) {
result.push(
<span key={`${entry.key}-${index}`}>{highlightedWords} </span>
);
continue outer;
}
}
}
result.push(<span key={index}>{word} </span>);
}
return result;
}
function highlightWord(content: string, highlight: KeywordHighlight) {
const regex = new RegExp(`(${highlight.keywords.join('|')})`);
if (regex.test(content)) {
const split = content.split(regex);
return split
.map((item, index) => {
if (!item) {
return;
}
// all odd occurrences are matches, the rest remain unchanged
if (index % 2 === 0) {
return <span key={index}>{item}</span>;
}
return (
<span
key={`${highlight.key}-${index}`}
style={{ color: highlight.color }}
>
{item}
</span>
);
})
.filter(Boolean);
}
return null;
}
const Keyword = styled.span`
color: #d4656b;
`;
const Punctuation = styled.span`
color: #81ceee;
`;
const blink = keyframes`
0% {
opacity: 0;
}
`;
const Cursor = styled.span`
display: inline-block;
width: 2px;
height: 15px;
background: #ffffff;
vertical-align: middle;
animation: ${blink} 1.5s steps(2) infinite;
`;
const LineNumbers = styled.div`
user-select: none;
width: 55px;
`;
interface LineNumberProps {
active?: boolean;
}
const LineNumber = styled.div<LineNumberProps>`
background: ${p => (p.active ? 'rgba(0, 0, 0, 0.3)' : 'none')};
color: ${p =>
p.active ? 'rgba(255, 255, 255, 0.6)' : 'rgba(255, 255, 255, 0.3)'};
text-align: right;
padding-right: 20px;
&:before {
content: attr(data-line-number);
}
`;
const Code = styled.div`
width: 100%;
`;
const ActiveLine = styled.div`
background: rgba(0, 0, 0, 0.3);
width: 100%;
`;