Skip to content

Commit d2229f2

Browse files
authored
Merge pull request #197 from stasm/dedent
Dedent multiline text to preserve content indentation
2 parents bf46e18 + 8bac02c commit d2229f2

File tree

5 files changed

+177
-20
lines changed

5 files changed

+177
-20
lines changed

lib/mappers.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export const join = list =>
2020
.filter(value => value !== Symbol.for("eof"))
2121
.join("");
2222

23+
// Prune unmatched maybes from a list.
24+
export const prune = list =>
25+
list.filter(value => value !== null);
26+
2327
// Map a list of {name, value} aliases into an array of values.
2428
export const keep_abstract = list =>
2529
list

syntax/abstract.mjs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function list_into(Type) {
4848
case FTL.Pattern:
4949
return elements =>
5050
always(new FTL.Pattern(
51-
elements
51+
dedent(elements)
5252
.reduce(join_adjacent(FTL.TextElement), [])
5353
.map(trim_text_at_extremes)
5454
.filter(remove_empty_text)));
@@ -111,33 +111,38 @@ export function into(Type) {
111111
}
112112
}
113113

114+
// Create a reducer suitable for joining adjacent nodes of the same type, if
115+
// type is one of types specified.
114116
function join_adjacent(...types) {
115117
return function(acc, cur) {
116118
let prev = acc[acc.length - 1];
117119
for (let Type of types) {
118120
if (prev instanceof Type && cur instanceof Type) {
119-
join_of_type(Type, prev, cur);
121+
// Replace prev with a new node of the same type whose value is
122+
// the sum of prev and cur, and discard cur.
123+
acc[acc.length - 1] = join_of_type(Type, prev, cur);
120124
return acc;
121125
}
122126
}
123127
return acc.concat(cur);
124128
};
125129
}
126130

131+
// Join values of two or more nodes of the same type. Return a new node.
127132
function join_of_type(Type, ...elements) {
128133
// TODO Join annotations and spans.
129134
switch (Type) {
130135
case FTL.TextElement:
131136
return elements.reduce((a, b) =>
132-
(a.value += b.value, a));
137+
new Type(a.value + b.value));
133138
case FTL.Comment:
134139
case FTL.GroupComment:
135140
case FTL.ResourceComment:
136141
return elements.reduce((a, b) =>
137-
(a.content += `\n${b.content}`, a));
142+
new Type(a.content + "\n" + b.content));
138143
case FTL.Junk:
139144
return elements.reduce((a, b) =>
140-
(a.content += b.content, a));
145+
new Type(a.content + b.content));
141146
}
142147
}
143148

@@ -154,18 +159,38 @@ function attach_comments(acc, cur) {
154159
}
155160
}
156161

157-
const LEADING_BLANK = /^[ \n\r]*/;
158-
const TRAILING_BLANK = /[ \n\r]*$/;
162+
// Remove the largest common indentation from a list of elements of a Pattern.
163+
// The indents are parsed in grammar.mjs and passed to abstract.mjs as string
164+
// primitives along with other PatternElements.
165+
function dedent(elements) {
166+
// Calculate the maximum common indent.
167+
let indents = elements.filter(element => typeof element === "string");
168+
let common = Math.min(...indents.map(indent => indent.length));
169+
170+
function trim_indents(element) {
171+
if (typeof element === "string") {
172+
// Trim the indent and convert it to a proper TextElement.
173+
// It will be joined with its adjacents later on.
174+
return new FTL.TextElement(element.slice(common));
175+
}
176+
return element;
177+
}
178+
179+
return elements.map(trim_indents);
180+
}
181+
182+
const LEADING_BLANK_BLOCK = /^\n*/;
183+
const TRAILING_BLANK_INLINE = / *$/;
159184

160185
function trim_text_at_extremes(element, index, array) {
161186
if (element instanceof FTL.TextElement) {
162187
if (index === 0) {
163188
element.value = element.value.replace(
164-
LEADING_BLANK, "");
189+
LEADING_BLANK_BLOCK, "");
165190
}
166191
if (index === array.length - 1) {
167192
element.value = element.value.replace(
168-
TRAILING_BLANK, "");
193+
TRAILING_BLANK_INLINE, "");
169194
}
170195
}
171196
return element;

syntax/grammar.mjs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
regex, repeat, repeat1, sequence, string
66
} from "../lib/combinators.mjs";
77
import {
8-
element_at, flatten, join, keep_abstract, mutate, print
8+
element_at, flatten, join, keep_abstract, mutate, print, prune
99
} from "../lib/mappers.mjs";
1010

1111
/* ----------------------------------------------------- */
@@ -154,12 +154,11 @@ let inline_text = defer(() =>
154154

155155
let block_text = defer(() =>
156156
sequence(
157-
blank_block.chain(into(FTL.TextElement)).abstract,
157+
blank_block.chain(into(FTL.TextElement)),
158158
blank_inline,
159-
indented_char.chain(into(FTL.TextElement)).abstract,
160-
maybe(inline_text.abstract)
161-
)
162-
.map(keep_abstract));
159+
indented_char.chain(into(FTL.TextElement)),
160+
maybe(inline_text))
161+
.map(prune));
163162

164163
let inline_placeable = defer(() =>
165164
sequence(
@@ -176,10 +175,10 @@ let inline_placeable = defer(() =>
176175

177176
let block_placeable = defer(() =>
178177
sequence(
179-
blank_block.chain(into(FTL.TextElement)).abstract,
180-
maybe(blank_inline),
181-
inline_placeable.abstract)
182-
.map(keep_abstract));
178+
blank_block.chain(into(FTL.TextElement)),
179+
// No indent before a placeable counts as 0 in dedention logic.
180+
maybe(blank_inline).map(s => s || ""),
181+
inline_placeable));
183182

184183
/* ------------------------------------------------------------------- */
185184
/* Rules for validating expressions in Placeables and as selectors of

test/fixtures/multiline_values.ftl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,28 @@ key07 =
3333
{"A multiline value"} starting and ending {"with a placeable"}
3434
3535
key08 = Leading and trailing whitespace.
36+
37+
key09 = zero
38+
three
39+
two
40+
one
41+
zero
42+
43+
key10 =
44+
two
45+
zero
46+
four
47+
48+
key11 =
49+
50+
51+
two
52+
zero
53+
54+
key12 =
55+
{"."}
56+
four
57+
58+
key13 =
59+
four
60+
{"."}

test/fixtures/multiline_values.json

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"elements": [
103103
{
104104
"type": "TextElement",
105-
"value": "A multiline value with non-standard\n\nindentation."
105+
"value": "A multiline value with non-standard\n\n indentation."
106106
}
107107
]
108108
},
@@ -212,6 +212,110 @@
212212
},
213213
"attributes": [],
214214
"comment": null
215+
},
216+
{
217+
"type": "Message",
218+
"id": {
219+
"type": "Identifier",
220+
"name": "key09"
221+
},
222+
"value": {
223+
"type": "Pattern",
224+
"elements": [
225+
{
226+
"type": "TextElement",
227+
"value": "zero\n three\n two\n one\nzero"
228+
}
229+
]
230+
},
231+
"attributes": [],
232+
"comment": null
233+
},
234+
{
235+
"type": "Message",
236+
"id": {
237+
"type": "Identifier",
238+
"name": "key10"
239+
},
240+
"value": {
241+
"type": "Pattern",
242+
"elements": [
243+
{
244+
"type": "TextElement",
245+
"value": " two\nzero\n four"
246+
}
247+
]
248+
},
249+
"attributes": [],
250+
"comment": null
251+
},
252+
{
253+
"type": "Message",
254+
"id": {
255+
"type": "Identifier",
256+
"name": "key11"
257+
},
258+
"value": {
259+
"type": "Pattern",
260+
"elements": [
261+
{
262+
"type": "TextElement",
263+
"value": " two\nzero"
264+
}
265+
]
266+
},
267+
"attributes": [],
268+
"comment": null
269+
},
270+
{
271+
"type": "Message",
272+
"id": {
273+
"type": "Identifier",
274+
"name": "key12"
275+
},
276+
"value": {
277+
"type": "Pattern",
278+
"elements": [
279+
{
280+
"type": "Placeable",
281+
"expression": {
282+
"type": "StringLiteral",
283+
"value": "."
284+
}
285+
},
286+
{
287+
"type": "TextElement",
288+
"value": "\n four"
289+
}
290+
]
291+
},
292+
"attributes": [],
293+
"comment": null
294+
},
295+
{
296+
"type": "Message",
297+
"id": {
298+
"type": "Identifier",
299+
"name": "key13"
300+
},
301+
"value": {
302+
"type": "Pattern",
303+
"elements": [
304+
{
305+
"type": "TextElement",
306+
"value": " four\n"
307+
},
308+
{
309+
"type": "Placeable",
310+
"expression": {
311+
"type": "StringLiteral",
312+
"value": "."
313+
}
314+
}
315+
]
316+
},
317+
"attributes": [],
318+
"comment": null
215319
}
216320
]
217321
}

0 commit comments

Comments
 (0)