Skip to content

Commit f0bee95

Browse files
committed
simplify docstrings
1 parent e37b59b commit f0bee95

File tree

1 file changed

+56
-81
lines changed

1 file changed

+56
-81
lines changed

src/components/composer/composer/prettifier_content.ts

Lines changed: 56 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,91 +3,81 @@ import {
33
leftOperandNeedsParenthesis,
44
rightOperandNeedsParenthesis,
55
} from "../../../formulas/parser";
6+
import { memoize } from "../../../helpers";
67

78
/**
8-
* This file implements a prettifier for Abstract Syntax Trees (ASTs) used in
9-
* spreadsheet formulas. The prettifier converts an AST into a human-readable
10-
* string representation, ensuring proper formatting with indentation, line
11-
* breaks, and grouping based on available width constraints.
9+
* Pretty-prints formula ASTs into readable formulas.
1210
*
13-
* The core functionality revolves around the `Doc` structure, which represents
14-
* formatting elements such as concatenation, nesting, and line insertion. The
15-
* prettifier dynamically selects the best formatting option based on the
16-
* available width, producing a compact or expanded representation as needed.
11+
* Implements a Wadler-inspired pretty printer:
12+
* it converts an AST into a `Doc` structure,
13+
* and then chooses between compact (flat) or expanded (with
14+
* line breaks and indentation) layouts depending on space.
1715
*
18-
* The implementation is inspired by the blog article:
19-
* https://lik.ai/blog/how-a-pretty-printer-works/
20-
*
21-
* Key components:
22-
* - `Doc` structure: Represents formatting options for ASTs.
23-
* - `print`: Converts a `Doc` into a string representation that fits within a
24-
* specified width.
25-
* - `astToDoc`: Transforms an AST into a `Doc` structure for formatting.
26-
* - Dynamic line breaking and indentation: Ensures the output is readable and
27-
* fits within constraints.
16+
* References:
17+
* - https://lik.ai/blog/how-a-pretty-printer-works/
18+
* - Wadler, "A prettier printer": https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
2819
*/
29-
30-
export function prettify(ast: AST) {
20+
export function prettify(ast: AST): string {
3121
return "=" + print(astToDoc(ast), 59); // 59 but 60 with the `=` at the beginning
3222
}
3323

3424
// ---------------------------------------
3525
// Doc structure
3626
// ---------------------------------------
3727

38-
// The `Doc` structure represents the possible ways to format an Abstract Syntax
39-
// Tree (AST) into a human-readable string.
40-
// It includes formatting elements such as line breaks, indentation, and token
41-
// grouping.
42-
//
43-
// Once created, this structure is used to determine the best formatting path
44-
// based on the available width, allowing dynamic selection of the most suitable
45-
// string representation for the given constraints.
46-
28+
/**
29+
* A `Doc` represents alternative layouts (tree of layouts) for pretty-printing.
30+
* The printer chooses the layout that fits best within the available width.
31+
*/
4732
type Doc = string | ChooseBetween | Concat | Nest | InsertLine;
4833

4934
type ChooseBetween = { type: "chooseBetween"; doc1: Doc; doc2: Doc };
5035
type Concat = { type: "concat"; docs: Doc[] };
5136
type Nest = { type: "nest"; indentLevel: number; doc: Doc };
5237
type InsertLine = { type: "insertLine" };
5338

54-
/** Represents a line break that can be inserted when formatting the document.
55-
* Used in groups to dynamically decide where to break lines based on available space.
39+
/**
40+
* A possible line break.
41+
* Printed as either space or newline.
5642
*/
5743
function line(): InsertLine {
5844
return { type: "insertLine" };
5945
}
6046

61-
/** Represents a nested structure with a specific indentation level.
62-
* Useful for formatting hierarchical or indented content.
47+
/**
48+
* Increase indentation for a nested block.
6349
*/
6450
function nest(indentLevel: number, doc: Doc): Nest {
6551
return { type: "nest", indentLevel, doc };
6652
}
6753

68-
/** Concatenates multiple `Doc` elements into a single `Doc`.
69-
* This is the primary way to combine strings and other formatting elements.
54+
/**
55+
* Combines multiple docs into a single doc, concatenating them side by side
56+
* without any line breaks or indentation.
7057
*/
7158
function concat(docs: Doc[]): Concat {
7259
return { type: "concat", docs };
7360
}
7461

75-
/** Groups a `Doc` to allow dynamic selection between a flattened version
76-
* (single-line representation) and the original structure based on available space.
62+
/**
63+
* Marks a document as a unit to be printed "flat" (all on one line)
64+
* if it fits within the width, otherwise with line breaks.
7765
*/
7866
function group(doc: Doc): ChooseBetween {
7967
return chooseBetween(flatten(doc), doc);
8068
}
8169

82-
/** Creates a choice between two `Doc` structures, typically used in `group`.
83-
* The choice depends on whether the available space can accommodate the flattened version.
70+
/**
71+
* Creates a choice between two alternative layouts.
72+
* The formatter tries `doc1`; if it does not fit within
73+
* the line width, it falls back to `doc2`.
8474
*/
8575
function chooseBetween(doc1: Doc, doc2: Doc): ChooseBetween {
8676
return { type: "chooseBetween", doc1, doc2 };
8777
}
8878

89-
/** Flattens a `Doc` structure into a single-line representation.
90-
* Used exclusively for `group` to create the flattened alternative.
79+
/**
80+
* Flattens a doc into its single-line form.
9181
*/
9282
function flatten(doc: Doc): Doc {
9383
if (typeof doc === "string") {
@@ -120,34 +110,27 @@ function flatten(doc: Doc): Doc {
120110

121111
/**
122112
* A linked list for string segments.
113+
* Used to avoid large string concatenations during layout selection.
123114
*/
124115
interface LinkedString {
125116
subString: string;
126117
next: LinkedString | null;
127118
}
128119

129-
/**
130-
* Memoized helper to build indentation strings.
131-
*/
132-
const memoizedIndentation: Record<number, string> = {};
133-
function getIndentation(indentLevel: number): string {
134-
if (!(indentLevel in memoizedIndentation)) {
135-
memoizedIndentation[indentLevel] = "\n" + "\t".repeat(indentLevel);
136-
}
137-
return memoizedIndentation[indentLevel];
138-
}
120+
const getIndentationString = memoize(function getIndentationString(indentLevel: number): string {
121+
return "\n" + "\t".repeat(indentLevel);
122+
});
139123

140124
/**
141-
* Converts a `Doc` structure into a string representation that fits within
125+
* Converts a `Doc` into a string representation that fits within
142126
* the specified width.
143127
*/
144128
function print(doc: Doc, width: number): string {
145-
return stringify(selectBestLinkedString(width, doc));
129+
return stringify(selectBestLayout(width, doc));
146130
}
147131

148132
/**
149-
* Recursively converts a `LinkedString` object into a human-readable string.
150-
* This is the final step of the prettifier process.
133+
* Join all segments of a LinkedString into the final string.
151134
*/
152135
function stringify(linkedString: LinkedString | null): string {
153136
let result = "";
@@ -159,16 +142,15 @@ function stringify(linkedString: LinkedString | null): string {
159142
}
160143

161144
/**
162-
* Selects the best `LinkedString` representation of a `Doc` that fits within
163-
* the given width.
145+
* Layout selection for a `Doc` that fits within the given width.
164146
*/
165-
function selectBestLinkedString(width: number, doc: Doc): LinkedString | null {
147+
function selectBestLayout(width: number, doc: Doc): LinkedString | null {
166148
const head: RestToFitNode = {
167149
indentLevel: 0,
168150
doc,
169151
next: null,
170152
};
171-
return _selectBestLinkedString(width, 0, head);
153+
return _selectBestLayout(width, 0, head);
172154
}
173155

174156
/**
@@ -180,7 +162,7 @@ interface RestToFitNode {
180162
next: RestToFitNode | null;
181163
}
182164

183-
function _selectBestLinkedString(
165+
function _selectBestLayout(
184166
width: number,
185167
currentIndentLevel: number,
186168
head: RestToFitNode | null
@@ -194,44 +176,44 @@ function _selectBestLinkedString(
194176
if (typeof doc === "string") {
195177
return {
196178
subString: doc,
197-
next: _selectBestLinkedString(width, currentIndentLevel + doc.length, next),
179+
next: _selectBestLayout(width, currentIndentLevel + doc.length, next),
198180
};
199181
}
200182
if (doc.type === "concat") {
201183
let newHead = next;
202184
for (let i = doc.docs.length - 1; i >= 0; i--) {
203185
newHead = { indentLevel, doc: doc.docs[i], next: newHead };
204186
}
205-
return _selectBestLinkedString(width, currentIndentLevel, newHead);
187+
return _selectBestLayout(width, currentIndentLevel, newHead);
206188
}
207189
if (doc.type === "nest") {
208-
return _selectBestLinkedString(width, currentIndentLevel, {
190+
return _selectBestLayout(width, currentIndentLevel, {
209191
indentLevel: indentLevel + doc.indentLevel,
210192
doc: doc.doc,
211193
next,
212194
});
213195
}
214196
if (doc.type === "insertLine") {
215197
return {
216-
subString: getIndentation(indentLevel),
217-
next: _selectBestLinkedString(width, indentLevel, next),
198+
subString: getIndentationString(indentLevel),
199+
next: _selectBestLayout(width, indentLevel, next),
218200
};
219201
}
220202
if (doc.type === "chooseBetween") {
221203
const head1 = { indentLevel, doc: doc.doc1, next };
222-
const possibleLinkedString = _selectBestLinkedString(width, currentIndentLevel, head1);
204+
const possibleLinkedString = _selectBestLayout(width, currentIndentLevel, head1);
223205
if (fits(width - currentIndentLevel, possibleLinkedString)) {
224206
return possibleLinkedString;
225207
}
226208

227209
const head2 = { indentLevel, doc: doc.doc2, next };
228-
return _selectBestLinkedString(width, currentIndentLevel, head2);
210+
return _selectBestLayout(width, currentIndentLevel, head2);
229211
}
230212
return null;
231213
}
232214

233215
/**
234-
* Checks whether a given `LinkedString` fits within the specified width.
216+
* Check if a layout fits on a single line within width.
235217
*/
236218
function fits(width: number, linkedString: LinkedString | null): boolean {
237219
while (linkedString) {
@@ -245,13 +227,6 @@ function fits(width: number, linkedString: LinkedString | null): boolean {
245227
return true;
246228
}
247229

248-
// ---------------------------------------
249-
// AST part
250-
// ---------------------------------------
251-
252-
/**
253-
* Converts an AST into a `Doc` structure for formatting.
254-
*/
255230
function astToDoc(ast: AST): Doc {
256231
switch (ast.type) {
257232
case "NUMBER":
@@ -268,7 +243,7 @@ function astToDoc(ast: AST): Doc {
268243

269244
case "FUNCALL":
270245
const docs = ast.args.map(astToDoc);
271-
return splitParenthesesContent(
246+
return wrapInParentheses(
272247
concat(docs.map((doc, i) => (i < 1 ? doc : concat([", ", line(), doc])))),
273248
ast.value
274249
);
@@ -278,7 +253,7 @@ function astToDoc(ast: AST): Doc {
278253
const needParenthesis = ast.postfix
279254
? leftOperandNeedsParenthesis(ast)
280255
: rightOperandNeedsParenthesis(ast);
281-
const finalOperandDoc = needParenthesis ? splitParenthesesContent(operandDoc) : operandDoc;
256+
const finalOperandDoc = needParenthesis ? wrapInParentheses(operandDoc) : operandDoc;
282257

283258
return ast.postfix
284259
? concat([finalOperandDoc, ast.value])
@@ -287,11 +262,11 @@ function astToDoc(ast: AST): Doc {
287262
case "BIN_OPERATION": {
288263
const leftDoc = astToDoc(ast.left);
289264
const needParenthesisLeftDoc = leftOperandNeedsParenthesis(ast);
290-
const finalLeftDoc = needParenthesisLeftDoc ? splitParenthesesContent(leftDoc) : leftDoc;
265+
const finalLeftDoc = needParenthesisLeftDoc ? wrapInParentheses(leftDoc) : leftDoc;
291266

292267
const rightDoc = astToDoc(ast.right);
293268
const needParenthesisRightDoc = rightOperandNeedsParenthesis(ast);
294-
const finalRightDoc = needParenthesisRightDoc ? splitParenthesesContent(rightDoc) : rightDoc;
269+
const finalRightDoc = needParenthesisRightDoc ? wrapInParentheses(rightDoc) : rightDoc;
295270

296271
const operator = `${ast.value}`;
297272
return group(concat([finalLeftDoc, operator, nest(1, concat([line(), finalRightDoc]))]));
@@ -306,9 +281,9 @@ function astToDoc(ast: AST): Doc {
306281
}
307282

308283
/**
309-
* Wraps a `Doc` in parentheses and optionally prepends a function name.
284+
* Wraps a `Doc` in parentheses (with optional function name).
310285
*/
311-
function splitParenthesesContent(doc: Doc, functionName: undefined | string = undefined): Doc {
286+
function wrapInParentheses(doc: Doc, functionName: undefined | string = undefined): Doc {
312287
const docToConcat = ["(", nest(1, concat([line(), doc])), line(), ")"];
313288
if (functionName) {
314289
docToConcat.unshift(functionName);

0 commit comments

Comments
 (0)