Path: blob/main/web/ui/src/features/river-js/RiverValue.tsx
5371 views
import { FC, Fragment, ReactElement } from 'react';12import { ObjectField, Value, ValueType } from './types';34import styles from './RiverValue.module.css';56export interface RiverValueProps {7value: Value;8}910/**11* RiverValue emits a paragraph which represents a River value.12*/13export const RiverValue: FC<RiverValueProps> = (props) => {14return (15<p className={styles.value}>16<ValueRenderer value={props.value} indentLevel={0} />17</p>18);19};2021type valueRendererProps = RiverValueProps & {22indentLevel: number;23};2425const ValueRenderer: FC<valueRendererProps> = (props) => {26const value = props.value;2728switch (value.type) {29case ValueType.NULL:30return <span className={styles.literal}>null</span>;3132case ValueType.NUMBER:33return <span className={styles.literal}>{value.value.toString()}</span>;3435case ValueType.STRING:36return <span className={styles.string}>"{escapeString(value.value)}"</span>;3738case ValueType.BOOL:39if (value.value) {40return <span className={styles.literal}>true</span>;41}42return <span className={styles.literal}>false</span>;4344case ValueType.ARRAY:45return (46<>47<span>[</span>48{value.value.map((element, idx) => {49return (50<Fragment key={idx.toString()}>51<ValueRenderer value={element} indentLevel={props.indentLevel} />52{idx + 1 < value.value.length ? <span>, </span> : null}53</Fragment>54);55})}56<span>]</span>57</>58);5960case ValueType.OBJECT:61if (value.value.length === 0) {62// No elements; return `{}` without any line breaks.63return (64<>65<span>{</span>66<span>}</span>67</>68);69}7071const partitions = partitionFields(value.value);7273return (74<>75<span>{</span>76<br />77{partitions.map((partition) => {78// Find the maximum field length across all fields in this79// partition.80const keyLength = partitionKeyLength(partition);8182return partition.map((element, index) => {83return (84<Fragment key={index.toString()}>85{getLinePrefix(props.indentLevel + 1)}86<span>{partitionKey(element, keyLength)} = </span>87<ValueRenderer value={element.value} indentLevel={props.indentLevel + 1} />88<span>,</span>89<br />90</Fragment>91);92});93})}94{getLinePrefix(props.indentLevel)}95<span>}</span>96</>97);9899case ValueType.FUNCTION:100return <span className={styles.special}>{value.value}</span>;101102case ValueType.CAPSULE:103return <span className={styles.special}>{value.value}</span>;104}105};106107/**108* partitionFields partitions fields in an object by fields which should have109* their equal signs aligned.110*111* A field which crosses multiple lines (i.e., recursively contains an object112* with more than one element) will cause a partition break, placing subsequent113* fields in another partition.114*/115function partitionFields(fields: ObjectField[]): ObjectField[][] {116const partitions = [];117118let currentPartition: ObjectField[] = [];119fields.forEach((field) => {120currentPartition.push(field);121122if (multilinedValue(field.value)) {123// Fields which cross multiple lines cause a partition break.124partitions.push(currentPartition);125currentPartition = [];126}127});128129if (currentPartition.length !== 0) {130partitions.push(currentPartition);131}132133return partitions;134}135136/** multilinedValue returns true if value recursively crosses multiple lines. */137function multilinedValue(value: Value): boolean {138switch (value.type) {139case ValueType.OBJECT:140// River objects cross more than one line whenever there is at least one141// element.142return value.value.length > 0;143144case ValueType.ARRAY:145// River arrays cross more than one line if any of their elements cross146// more than one line.147return value.value.some((v) => multilinedValue(v));148}149150// Other values never cross line barriers.151return false;152}153154/**155* partitionKeyLength returns the length of keys within the partition. The156* length is determined by the longest field name in the partition.157*/158function partitionKeyLength(partition: ObjectField[]): number {159let keyLength = 0;160161partition.forEach((f) => {162const fieldLength = partitionKey(f, 0).length;163if (fieldLength > keyLength) {164keyLength = fieldLength;165}166});167168return keyLength;169}170171/**172* partitionKey returns the text to use to display a key for a field within a173* partition.174*/175function partitionKey(field: ObjectField, keyLength: number): string {176let key = field.key;177if (!validIdentifier(key)) {178// Keys which aren't valid identifiers should be wrapped in quotes.179key = `"${key}"`;180}181182if (key.length < keyLength) {183return key + ' '.repeat(keyLength - key.length);184}185return key;186}187188function getLinePrefix(indentLevel: number): ReactElement | null {189if (indentLevel === 0) {190return null;191}192return <span>{'\t'.repeat(indentLevel)}</span>;193}194195/**196* validIdentifier reports whether the input is a valid River identifier.197*/198function validIdentifier(input: string): boolean {199return /^[_a-z][_a-z0-9]*$/i.test(input);200}201202/**203* escapeString escapes special characters in a string so they can be printed204* inside a River string literal.205*/206function escapeString(input: string): string {207// TODO(rfratto): this should also escape Unicode characters into \u and \U208// forms.209return input.replace(/[\b\f\n\r\t\v\0'"\\]/g, (match) => {210switch (match) {211case '\b':212return '\\b';213case '\f':214return '\\f';215case '\n':216return '\\n';217case '\r':218return '\\r';219case '\t':220return '\\t';221case '\v':222return '\\v';223case "'":224return "\\'";225case '"':226return '\\"';227case '\\':228return '\\\\';229}230return '';231});232}233234235