Path: blob/main/web/ui/src/features/river-js/stringify.ts
5294 views
import { ObjectField, Value, ValueType } from './types';12/**3* Returns a native River config representation of the given Value.4*/5export function riverStringify(v: Value): string {6return riverStringifyImpl(v, 0);7}89function riverStringifyImpl(v: Value, indent: number): string {10switch (v.type) {11case ValueType.NULL: {12return 'null';13}1415case ValueType.NUMBER: {16return v.value.toString();17}1819case ValueType.STRING: {20return `"${escapeString(v.value)}"`;21}2223case ValueType.BOOL: {24if (v.value) {25return 'true';26} else {27return 'false';28}29}3031case ValueType.ARRAY: {32let result = '[';33v.value.forEach((element, idx) => {34result += riverStringifyImpl(element, indent);35if (idx + 1 < v.value.length) {36result += ', ';37}38});39result += ']';40return result;41}4243case ValueType.OBJECT: {44if (v.value.length === 0) {45return '{}';46}4748const partitions = partitionFields(v.value);4950let result = '{\n';5152partitions.forEach((partition) => {53// Find the maximum field length across all fields in this partition.54const keyLength = partitionKeyLength(partition);5556return partition.forEach((element) => {57result += indentLine(indent + 1);58result += `${partitionKey(element, keyLength)} = ${riverStringifyImpl(element.value, indent + 1)}`;59result += ',\n';60});61});6263result += indentLine(indent) + '}';64return result;65}6667case ValueType.FUNCTION: {68return v.value;69}7071case ValueType.CAPSULE: {72return v.value;73}7475default: {76return 'null';77}78}79}8081/**82* escapeString escapes special characters in a string so they can be printed83* inside a River string literal.84*/85function escapeString(input: string): string {86// TODO(rfratto): this should also escape Unicode characters into \u and \U87// forms.88return input.replace(/[\b\f\n\r\t\v\0'"\\]/g, (match) => {89switch (match) {90case '\b':91return '\\b';92case '\f':93return '\\f';94case '\n':95return '\\n';96case '\r':97return '\\r';98case '\t':99return '\\t';100case '\v':101return '\\v';102case "'":103return "\\'";104case '"':105return '\\"';106case '\\':107return '\\\\';108}109return '';110});111}112113function indentLine(indentLevel: number): string {114if (indentLevel === 0) {115return '';116}117return '\t'.repeat(indentLevel);118}119120/**121* partitionFields partitions fields in an object by fields which should have122* their equal signs aligned.123*124* A field which crosses multiple lines (i.e., recursively contains an object125* with more than one element) will cause a partition break, placing subsequent126* fields in another partition.127*/128function partitionFields(fields: ObjectField[]): ObjectField[][] {129const partitions = [];130131let currentPartition: ObjectField[] = [];132fields.forEach((field) => {133currentPartition.push(field);134135if (multilinedValue(field.value)) {136// Fields which cross multiple lines cause a partition break.137partitions.push(currentPartition);138currentPartition = [];139}140});141142if (currentPartition.length !== 0) {143partitions.push(currentPartition);144}145146return partitions;147}148149/** multilinedValue returns true if value recrusively crosses multiple lines. */150function multilinedValue(value: Value): boolean {151switch (value.type) {152case ValueType.OBJECT:153// River objects cross more than one line whenever there is at least one154// element.155return value.value.length > 0;156157case ValueType.ARRAY:158// River arrays cross more than one line if any of their elements cross159// more than one line.160return value.value.some((v) => multilinedValue(v));161}162163// Other values never cross line barriers.164return false;165}166167/**168* partitionKeyLength returns the length of keys within the partition. The169* length is determined by the longest field name in the partition.170*/171function partitionKeyLength(partition: ObjectField[]): number {172let keyLength = 0;173174partition.forEach((f) => {175const fieldLength = partitionKey(f, 0).length;176if (fieldLength > keyLength) {177keyLength = fieldLength;178}179});180181return keyLength;182}183184/**185* partitionKey returns the text to use to display a key for a field within a186* partition.187*/188function partitionKey(field: ObjectField, keyLength: number): string {189let key = field.key;190if (!validIdentifier(key)) {191// Keys which aren't valid identifiers should be wrapped in quotes.192key = `"${key}"`;193}194195if (key.length < keyLength) {196return key + ' '.repeat(keyLength - key.length);197}198return key;199}200201/**202* validIdentifier reports whether the input is a valid River identifier.203*/204function validIdentifier(input: string): boolean {205return /^[_a-z][_a-z0-9]*$/i.test(input);206}207208209