/** * 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) { const [activeTabIndex, setActiveTabIndex] = useState(0); const files = React.Children.map( props.children, (child: React.ReactElement) => { 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( ); } } else { lineNumbers.push(); } return ( {lineNumbers} {parsed} ); } 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(
{highlightValue(line, highlights)}
); continue; } result.push(
 
); } 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 (
{code.substring(0, symbolIndex)} {symbol} {highlightValue(value, highlights)}
); } 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 {item}; } return ( {item} ); }) .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( {highlightedWords} ); continue outer; } } } result.push({word} ); } 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 {item}; } return ( {item} ); }) .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` 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%; `;