1 | import { tail, compose, pathOr, map, concat, transduce, pipe, trim, split, join, curry } from 'ramda';
|
---|
2 | import { isInteger, trimStart, trimEnd, isUndefined, trimCharsStart, isEmptyString, repeatStr, concatRight } from 'ramda-adjunct';
|
---|
3 | import { unraw } from 'unraw';
|
---|
4 |
|
---|
5 | /**
|
---|
6 | * Helpers.
|
---|
7 | */
|
---|
8 |
|
---|
9 | const blockStyleRegExp = /^(?<style>[|>])(?<chomping>[+-]?)(?<indentation>[0-9]*)\s/;
|
---|
10 | const getIndentationIndicator = content => {
|
---|
11 | const matches = content.match(blockStyleRegExp);
|
---|
12 | const indicator = pathOr('', ['groups', 'indentation'], matches);
|
---|
13 | return isEmptyString(indicator) ? undefined : parseInt(indicator, 10);
|
---|
14 | };
|
---|
15 | const getIndentation = content => {
|
---|
16 | const explicitIndentationIndicator = getIndentationIndicator(content);
|
---|
17 |
|
---|
18 | // we have explicit indentation indicator
|
---|
19 | if (isInteger(explicitIndentationIndicator)) {
|
---|
20 | return repeatStr(' ', explicitIndentationIndicator);
|
---|
21 | }
|
---|
22 |
|
---|
23 | // we assume indentation indicator from first line
|
---|
24 | const firstLine = pathOr('', [1], content.split('\n'));
|
---|
25 | const implicitIndentationIndicator = pathOr(0, ['groups', 'indentation', 'length'], firstLine.match(/^(?<indentation>[ ]*)/));
|
---|
26 | return repeatStr(' ', implicitIndentationIndicator);
|
---|
27 | };
|
---|
28 | const getChompingIndicator = content => {
|
---|
29 | const matches = content.match(blockStyleRegExp);
|
---|
30 | const indicator = pathOr('', ['groups', 'chomping'], matches);
|
---|
31 | return isEmptyString(indicator) ? undefined : indicator;
|
---|
32 | };
|
---|
33 | const chomp = (indicator, content) => {
|
---|
34 | // clip (single newline at end)
|
---|
35 | if (isUndefined(indicator)) {
|
---|
36 | return `${trimEnd(content)}\n`;
|
---|
37 | }
|
---|
38 | // strip (no newline at end)
|
---|
39 | if (indicator === '-') {
|
---|
40 | return trimEnd(content);
|
---|
41 | }
|
---|
42 | // keep (all newlines from end)
|
---|
43 | if (indicator === '+') {
|
---|
44 | return content;
|
---|
45 | }
|
---|
46 | return content;
|
---|
47 | };
|
---|
48 |
|
---|
49 | /**
|
---|
50 | * Normalizes lines breaks.
|
---|
51 | * https://yaml.org/spec/1.2/spec.html#line%20break/normalization/
|
---|
52 | */
|
---|
53 | // @ts-ignore
|
---|
54 | const normalizeLineBreaks = val => val.replace(/\r\n/g, '\n');
|
---|
55 |
|
---|
56 | // prevent escaped line breaks from being converted to a space
|
---|
57 | const preventLineBreakCollapseToSpace = val => val.replace(/\\\n\s*/g, '');
|
---|
58 |
|
---|
59 | // collapse line breaks into spaces
|
---|
60 | const collapseLineBreakToSpace = val => {
|
---|
61 | /**
|
---|
62 | * Safari doesn't support negative lookbehind, thus we use mimicking technique:
|
---|
63 | *
|
---|
64 | * - https://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript
|
---|
65 | *
|
---|
66 | * Ideally we want to use following replace, but that's not currently possible:
|
---|
67 | *
|
---|
68 | * .replace(/[^\n]\n([^\n]+)/g, (match: string, p1: string) => ` ${p1.trimLeft()}`)
|
---|
69 | */
|
---|
70 | return val.replace(/(\n)?\n([^\n]+)/g, (match, p1, p2) => p1 ? match : ` ${p2.trimStart()}`).replace(/[\n]{2}/g, '\n');
|
---|
71 | };
|
---|
72 | const removeQuotes = curry((quoteType, val) => val.replace(new RegExp(`^${quoteType}`), '').replace(new RegExp(`${quoteType}$`), ''));
|
---|
73 |
|
---|
74 | /**
|
---|
75 | * Formats Flow Scalar Plain style.
|
---|
76 | * https://yaml.org/spec/1.2/spec.html#id2788859
|
---|
77 | */
|
---|
78 | export const formatFlowPlain = pipe(normalizeLineBreaks, trim, collapseLineBreakToSpace, split('\n'), map(trimStart), join('\n'));
|
---|
79 |
|
---|
80 | /**
|
---|
81 | * Formats Flow Scalar Single-Quoted style.
|
---|
82 | * https://yaml.org/spec/1.2/spec.html#id2788097
|
---|
83 | */
|
---|
84 |
|
---|
85 | export const formatFlowSingleQuoted = pipe(normalizeLineBreaks, trim, collapseLineBreakToSpace, split('\n'), map(trimStart), join('\n'), removeQuotes("'"));
|
---|
86 |
|
---|
87 | /**
|
---|
88 | * Formats Flow Scalar Double-Quoted style.
|
---|
89 | * https://yaml.org/spec/1.2/spec.html#id2787109
|
---|
90 | */
|
---|
91 | export const formatFlowDoubleQuoted = pipe(normalizeLineBreaks, trim, preventLineBreakCollapseToSpace, collapseLineBreakToSpace, unraw, split('\n'), map(trimStart), join('\n'), removeQuotes('"'));
|
---|
92 |
|
---|
93 | /**
|
---|
94 | * Formats Block Scalar Literal style.
|
---|
95 | * https://yaml.org/spec/1.2/spec.html#id2795688
|
---|
96 | */
|
---|
97 | export const formatBlockLiteral = content => {
|
---|
98 | const indentation = getIndentation(content);
|
---|
99 | const chompingIndicator = getChompingIndicator(content);
|
---|
100 | const normalized = normalizeLineBreaks(content);
|
---|
101 | const lines = tail(normalized.split('\n')); // first line only contains indicators
|
---|
102 | const transducer = compose(map(trimCharsStart(indentation)), map(concatRight('\n')));
|
---|
103 | // @ts-ignore
|
---|
104 | const deindented = transduce(transducer, concat, '', lines);
|
---|
105 | return chomp(chompingIndicator, deindented);
|
---|
106 | };
|
---|
107 |
|
---|
108 | /**
|
---|
109 | * Formats BLock Scalar Folded style.
|
---|
110 | * https://yaml.org/spec/1.2/spec.html#id2796251
|
---|
111 | */
|
---|
112 | export const formatBlockFolded = content => {
|
---|
113 | const indentation = getIndentation(content);
|
---|
114 | const chompingIndicator = getChompingIndicator(content);
|
---|
115 | const normalized = normalizeLineBreaks(content);
|
---|
116 | const lines = tail(normalized.split('\n')); // first line only contains indicators
|
---|
117 | const transducer = compose(map(trimCharsStart(indentation)), map(concatRight('\n')));
|
---|
118 | // @ts-ignore
|
---|
119 | const deindented = transduce(transducer, concat, '', lines);
|
---|
120 | const collapsed = collapseLineBreakToSpace(deindented);
|
---|
121 | return chomp(chompingIndicator, collapsed);
|
---|
122 | }; |
---|