Skip to content

Commit

Permalink
generator API: extended definition of 'JoinOptions' used to configure…
Browse files Browse the repository at this point in the history
… executions of 'joinToNode'

* now allows specification of item prefixes and suffixes as fixed values, not only via provider functions
* now allows demanding to not append a newline after the last item of given iterable, provided 'appendNewLineIfNotEmpty' is set
  • Loading branch information
sailingKieler committed Oct 9, 2023
1 parent 4d11fcf commit f2a14f9
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 6 deletions.
18 changes: 12 additions & 6 deletions packages/langium/src/generator/node-joiner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { CompositeGeneratorNode, traceToNode } from './generator-node.js';

export interface JoinOptions<T> {
filter?: (element: T, index: number, isLast: boolean) => boolean;
prefix?: (element: T, index: number, isLast: boolean) => Generated | undefined;
suffix?: (element: T, index: number, isLast: boolean) => Generated | undefined;
prefix?: Generated | ((element: T, index: number, isLast: boolean) => Generated | undefined);
suffix?: Generated | ((element: T, index: number, isLast: boolean) => Generated | undefined);
separator?: Generated;
appendNewLineIfNotEmpty?: true;
skipNewLineAfterLastItem?: true;
}

const defaultToGenerated = (e: unknown): Generated => e === undefined || typeof e === 'string' || isGeneratorNode(e) ? e : String(e);
Expand Down Expand Up @@ -100,23 +101,28 @@ export function joinToNode<T>(
): CompositeGeneratorNode | undefined {

const toGenerated = typeof toGeneratedOrOptions === 'function' ? toGeneratedOrOptions : defaultToGenerated;
const { filter, prefix, suffix, separator, appendNewLineIfNotEmpty } = typeof toGeneratedOrOptions === 'object' ? toGeneratedOrOptions : options;
const { filter, prefix, suffix, separator, appendNewLineIfNotEmpty, skipNewLineAfterLastItem } = typeof toGeneratedOrOptions === 'object' ? toGeneratedOrOptions : options;

const prefixFunc = typeof prefix === 'function' ? prefix : undefined;
const suffixFunc = typeof suffix === 'function' ? suffix : undefined;

return reduceWithIsLast(iterable, (node, it, i, isLast) => {
if (filter && !filter(it, i, isLast)) {
return node;
}
const content = toGenerated(it, i, isLast);
return (node ??= new CompositeGeneratorNode())
.append(prefix && prefix(it, i, isLast))
.append(prefixFunc && prefixFunc(it, i, isLast) || prefix as Generated)
.append(content)
.append(suffix && suffix(it, i, isLast))
.append(suffixFunc && suffixFunc(it, i, isLast) || suffix as Generated)
.appendIf(!isLast && content !== undefined, separator)
.appendNewLineIfNotEmptyIf(
// append 'newLineIfNotEmpty' elements only if 'node' has some content already,
// as if the parent is an IndentNode with 'indentImmediately' set to 'false'
// the indentation is not properly applied to the first non-empty line of the (this) child node
!node.isEmpty() && !!appendNewLineIfNotEmpty
// besides, append the newLine only if more lines are following, if appending a newline
// is not suppressed for the final item
!node.isEmpty() && !!appendNewLineIfNotEmpty && (!isLast || !skipNewLineAfterLastItem)
);
});
}
Expand Down
59 changes: 59 additions & 0 deletions packages/langium/test/generator/template-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,17 @@ describe('Embedded forEach loops', () => {
`);
});
test('ForEach loop with element prefix and Array input', () => {
const node = n`
Data:
${joinToNode(['a', 'b'], String, { prefix: ',' })}
`;
const text = toString(node);
expect(text).toBe(s`
Data:
,a,b
`);
});
test('ForEach loop with element prefix and Array input 2 (function)', () => {
const node = n`
Data:
${joinToNode(['a', 'b'], String, { prefix: (e, i) => i === 0 ? '': ', ' })}
Expand All @@ -962,6 +973,17 @@ describe('Embedded forEach loops', () => {
`);
});
test('ForEach loop with element suffix and Set input', () => {
const node = n`
Data:
${joinToNode(new Set(['a', 'b']), String, { suffix: ',' })}
`;
const text = toString(node);
expect(text).toBe(s`
Data:
a,b,
`);
});
test('ForEach loop with element suffix and Set input 2 (function)', () => {
const node = n`
Data:
${joinToNode(new Set(['a', 'b']), String, { suffix: (e, i, isLast) => isLast ? '': ', ' })}
Expand Down Expand Up @@ -996,6 +1018,18 @@ describe('Embedded forEach loops', () => {
`);
});
test('ForEach loop with line breaks and skip line break after last line and Stream input', () => {
const node = n`
Data:
${joinToNode(stream(['a', 'b']), String, { appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true})}
`;
const text = toString(node);
expect(text).toBe(s`
Data:
a
b
`);
});
test('ForEach loop with separator, line breaks, and Stream input', () => {
const node = n`
Data:
Expand All @@ -1010,6 +1044,19 @@ describe('Embedded forEach loops', () => {
`);
});
test('ForEach loop with separator, line breaks and skip line break after last line, and Stream input', () => {
const node = n`
Data:
${joinToNode(stream(['a', undefined, 'b']), String, { separator: ',', appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true})}
`;
const text = toString(node);
expect(text).toBe(s`
Data:
a,
undefined,
b
`);
});
test('ForEach loop with omitted `undefined`, separator, line breaks and Stream input', () => {
const node = n`
Data:
Expand All @@ -1023,6 +1070,18 @@ describe('Embedded forEach loops', () => {
`);
});
test('ForEach loop with omitted `undefined`, separator, line breaks and skip line break after last line, and Stream input', () => {
const node = n`
Data:
${joinToNode(stream(['a', undefined, 'b']), e => e && String(e), { separator: ',', appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true})}
`;
const text = toString(node);
expect(text).toBe(s`
Data:
a,
b
`);
});
});

describe('Appending templates to existing nodes', () => {
Expand Down

0 comments on commit f2a14f9

Please sign in to comment.