448 lines
10 KiB
JavaScript
448 lines
10 KiB
JavaScript
import {
|
|
DOC_TYPE_STRING,
|
|
DOC_TYPE_ARRAY,
|
|
DOC_TYPE_CURSOR,
|
|
DOC_TYPE_INDENT,
|
|
DOC_TYPE_ALIGN,
|
|
DOC_TYPE_TRIM,
|
|
DOC_TYPE_GROUP,
|
|
DOC_TYPE_FILL,
|
|
DOC_TYPE_IF_BREAK,
|
|
DOC_TYPE_INDENT_IF_BREAK,
|
|
DOC_TYPE_LINE_SUFFIX,
|
|
DOC_TYPE_LINE_SUFFIX_BOUNDARY,
|
|
DOC_TYPE_LINE,
|
|
DOC_TYPE_LABEL,
|
|
DOC_TYPE_BREAK_PARENT,
|
|
} from "./constants.js";
|
|
import { literalline, join } from "./builders.js";
|
|
import getDocType from "./utils/get-doc-type.js";
|
|
import traverseDoc from "./utils/traverse-doc.js";
|
|
import InvalidDocError from "./invalid-doc-error.js";
|
|
|
|
const getDocParts = (doc) => {
|
|
if (Array.isArray(doc)) {
|
|
return doc;
|
|
}
|
|
|
|
/* c8 ignore next 3 */
|
|
if (doc.type !== DOC_TYPE_FILL) {
|
|
throw new Error(`Expect doc to be 'array' or '${DOC_TYPE_FILL}'.`);
|
|
}
|
|
|
|
return doc.parts;
|
|
};
|
|
|
|
function mapDoc(doc, cb) {
|
|
// Avoid creating `Map`
|
|
if (typeof doc === "string") {
|
|
return cb(doc);
|
|
}
|
|
|
|
// Within a doc tree, the same subtrees can be found multiple times.
|
|
// E.g., often this happens in conditional groups.
|
|
// As an optimization (those subtrees can be huge) and to maintain the
|
|
// reference structure of the tree, the mapping results are cached in
|
|
// a map and reused.
|
|
const mapped = new Map();
|
|
|
|
return rec(doc);
|
|
|
|
function rec(doc) {
|
|
if (mapped.has(doc)) {
|
|
return mapped.get(doc);
|
|
}
|
|
const result = process(doc);
|
|
mapped.set(doc, result);
|
|
return result;
|
|
}
|
|
|
|
function process(doc) {
|
|
switch (getDocType(doc)) {
|
|
case DOC_TYPE_ARRAY:
|
|
return cb(doc.map(rec));
|
|
|
|
case DOC_TYPE_FILL:
|
|
return cb({ ...doc, parts: doc.parts.map(rec) });
|
|
|
|
case DOC_TYPE_IF_BREAK:
|
|
return cb({
|
|
...doc,
|
|
breakContents: rec(doc.breakContents),
|
|
flatContents: rec(doc.flatContents),
|
|
});
|
|
|
|
case DOC_TYPE_GROUP: {
|
|
let { expandedStates, contents } = doc;
|
|
if (expandedStates) {
|
|
expandedStates = expandedStates.map(rec);
|
|
contents = expandedStates[0];
|
|
} else {
|
|
contents = rec(contents);
|
|
}
|
|
return cb({ ...doc, contents, expandedStates });
|
|
}
|
|
|
|
case DOC_TYPE_ALIGN:
|
|
case DOC_TYPE_INDENT:
|
|
case DOC_TYPE_INDENT_IF_BREAK:
|
|
case DOC_TYPE_LABEL:
|
|
case DOC_TYPE_LINE_SUFFIX:
|
|
return cb({ ...doc, contents: rec(doc.contents) });
|
|
|
|
case DOC_TYPE_STRING:
|
|
case DOC_TYPE_CURSOR:
|
|
case DOC_TYPE_TRIM:
|
|
case DOC_TYPE_LINE_SUFFIX_BOUNDARY:
|
|
case DOC_TYPE_LINE:
|
|
case DOC_TYPE_BREAK_PARENT:
|
|
return cb(doc);
|
|
|
|
default:
|
|
/* c8 ignore next 3 */
|
|
throw new InvalidDocError(doc);
|
|
}
|
|
}
|
|
}
|
|
|
|
function findInDoc(doc, fn, defaultValue) {
|
|
let result = defaultValue;
|
|
let shouldSkipFurtherProcessing = false;
|
|
function findInDocOnEnterFn(doc) {
|
|
if (shouldSkipFurtherProcessing) {
|
|
return false;
|
|
}
|
|
|
|
const maybeResult = fn(doc);
|
|
if (maybeResult !== undefined) {
|
|
shouldSkipFurtherProcessing = true;
|
|
result = maybeResult;
|
|
}
|
|
}
|
|
traverseDoc(doc, findInDocOnEnterFn);
|
|
return result;
|
|
}
|
|
|
|
function willBreakFn(doc) {
|
|
if (doc.type === DOC_TYPE_GROUP && doc.break) {
|
|
return true;
|
|
}
|
|
if (doc.type === DOC_TYPE_LINE && doc.hard) {
|
|
return true;
|
|
}
|
|
if (doc.type === DOC_TYPE_BREAK_PARENT) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function willBreak(doc) {
|
|
return findInDoc(doc, willBreakFn, false);
|
|
}
|
|
|
|
function breakParentGroup(groupStack) {
|
|
if (groupStack.length > 0) {
|
|
const parentGroup = groupStack.at(-1);
|
|
// Breaks are not propagated through conditional groups because
|
|
// the user is expected to manually handle what breaks.
|
|
if (!parentGroup.expandedStates && !parentGroup.break) {
|
|
// An alternative truthy value allows to distinguish propagated group breaks
|
|
// and not to print them as `group(..., { break: true })` in `--debug-print-doc`.
|
|
parentGroup.break = "propagated";
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function propagateBreaks(doc) {
|
|
const alreadyVisitedSet = new Set();
|
|
const groupStack = [];
|
|
function propagateBreaksOnEnterFn(doc) {
|
|
if (doc.type === DOC_TYPE_BREAK_PARENT) {
|
|
breakParentGroup(groupStack);
|
|
}
|
|
if (doc.type === DOC_TYPE_GROUP) {
|
|
groupStack.push(doc);
|
|
if (alreadyVisitedSet.has(doc)) {
|
|
return false;
|
|
}
|
|
alreadyVisitedSet.add(doc);
|
|
}
|
|
}
|
|
function propagateBreaksOnExitFn(doc) {
|
|
if (doc.type === DOC_TYPE_GROUP) {
|
|
const group = groupStack.pop();
|
|
if (group.break) {
|
|
breakParentGroup(groupStack);
|
|
}
|
|
}
|
|
}
|
|
traverseDoc(
|
|
doc,
|
|
propagateBreaksOnEnterFn,
|
|
propagateBreaksOnExitFn,
|
|
/* shouldTraverseConditionalGroups */ true,
|
|
);
|
|
}
|
|
|
|
function removeLinesFn(doc) {
|
|
// Force this doc into flat mode by statically converting all
|
|
// lines into spaces (or soft lines into nothing). Hard lines
|
|
// should still output because there's too great of a chance
|
|
// of breaking existing assumptions otherwise.
|
|
if (doc.type === DOC_TYPE_LINE && !doc.hard) {
|
|
return doc.soft ? "" : " ";
|
|
}
|
|
|
|
if (doc.type === DOC_TYPE_IF_BREAK) {
|
|
return doc.flatContents;
|
|
}
|
|
|
|
return doc;
|
|
}
|
|
|
|
function removeLines(doc) {
|
|
return mapDoc(doc, removeLinesFn);
|
|
}
|
|
|
|
function stripTrailingHardlineFromParts(parts) {
|
|
parts = [...parts];
|
|
|
|
while (
|
|
parts.length >= 2 &&
|
|
parts.at(-2).type === DOC_TYPE_LINE &&
|
|
parts.at(-1).type === DOC_TYPE_BREAK_PARENT
|
|
) {
|
|
parts.length -= 2;
|
|
}
|
|
|
|
if (parts.length > 0) {
|
|
const lastPart = stripTrailingHardlineFromDoc(parts.at(-1));
|
|
parts[parts.length - 1] = lastPart;
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
function stripTrailingHardlineFromDoc(doc) {
|
|
switch (getDocType(doc)) {
|
|
case DOC_TYPE_ALIGN:
|
|
case DOC_TYPE_INDENT:
|
|
case DOC_TYPE_INDENT_IF_BREAK:
|
|
case DOC_TYPE_GROUP:
|
|
case DOC_TYPE_LINE_SUFFIX:
|
|
case DOC_TYPE_LABEL: {
|
|
const contents = stripTrailingHardlineFromDoc(doc.contents);
|
|
return { ...doc, contents };
|
|
}
|
|
|
|
case DOC_TYPE_IF_BREAK:
|
|
return {
|
|
...doc,
|
|
breakContents: stripTrailingHardlineFromDoc(doc.breakContents),
|
|
flatContents: stripTrailingHardlineFromDoc(doc.flatContents),
|
|
};
|
|
|
|
case DOC_TYPE_FILL:
|
|
return { ...doc, parts: stripTrailingHardlineFromParts(doc.parts) };
|
|
|
|
case DOC_TYPE_ARRAY:
|
|
return stripTrailingHardlineFromParts(doc);
|
|
|
|
case DOC_TYPE_STRING:
|
|
return doc.replace(/[\n\r]*$/, "");
|
|
|
|
case DOC_TYPE_CURSOR:
|
|
case DOC_TYPE_TRIM:
|
|
case DOC_TYPE_LINE_SUFFIX_BOUNDARY:
|
|
case DOC_TYPE_LINE:
|
|
case DOC_TYPE_BREAK_PARENT:
|
|
// No op
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidDocError(doc);
|
|
}
|
|
|
|
return doc;
|
|
}
|
|
|
|
function stripTrailingHardline(doc) {
|
|
// HACK remove ending hardline, original PR: #1984
|
|
return stripTrailingHardlineFromDoc(cleanDoc(doc));
|
|
}
|
|
|
|
function cleanDocFn(doc) {
|
|
switch (getDocType(doc)) {
|
|
case DOC_TYPE_FILL:
|
|
if (doc.parts.every((part) => part === "")) {
|
|
return "";
|
|
}
|
|
break;
|
|
case DOC_TYPE_GROUP:
|
|
if (!doc.contents && !doc.id && !doc.break && !doc.expandedStates) {
|
|
return "";
|
|
}
|
|
// Remove nested only group
|
|
if (
|
|
doc.contents.type === DOC_TYPE_GROUP &&
|
|
doc.contents.id === doc.id &&
|
|
doc.contents.break === doc.break &&
|
|
doc.contents.expandedStates === doc.expandedStates
|
|
) {
|
|
return doc.contents;
|
|
}
|
|
break;
|
|
case DOC_TYPE_ALIGN:
|
|
case DOC_TYPE_INDENT:
|
|
case DOC_TYPE_INDENT_IF_BREAK:
|
|
case DOC_TYPE_LINE_SUFFIX:
|
|
if (!doc.contents) {
|
|
return "";
|
|
}
|
|
break;
|
|
case DOC_TYPE_IF_BREAK:
|
|
if (!doc.flatContents && !doc.breakContents) {
|
|
return "";
|
|
}
|
|
break;
|
|
case DOC_TYPE_ARRAY: {
|
|
// Flat array, concat strings
|
|
const parts = [];
|
|
for (const part of doc) {
|
|
if (!part) {
|
|
continue;
|
|
}
|
|
const [currentPart, ...restParts] = Array.isArray(part) ? part : [part];
|
|
if (
|
|
typeof currentPart === "string" &&
|
|
typeof parts.at(-1) === "string"
|
|
) {
|
|
parts[parts.length - 1] += currentPart;
|
|
} else {
|
|
parts.push(currentPart);
|
|
}
|
|
parts.push(...restParts);
|
|
}
|
|
|
|
if (parts.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
if (parts.length === 1) {
|
|
return parts[0];
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
case DOC_TYPE_STRING:
|
|
case DOC_TYPE_CURSOR:
|
|
case DOC_TYPE_TRIM:
|
|
case DOC_TYPE_LINE_SUFFIX_BOUNDARY:
|
|
case DOC_TYPE_LINE:
|
|
case DOC_TYPE_LABEL:
|
|
case DOC_TYPE_BREAK_PARENT:
|
|
// No op
|
|
break;
|
|
default:
|
|
/* c8 ignore next 3 */
|
|
throw new InvalidDocError(doc);
|
|
}
|
|
|
|
return doc;
|
|
}
|
|
// A safer version of `normalizeDoc`
|
|
// - `normalizeDoc` concat strings and flat array in `fill`, while `cleanDoc` don't
|
|
// - On array, `normalizeDoc` always return object with `parts`, `cleanDoc` may return strings
|
|
// - `cleanDoc` also remove nested `group`s and empty `fill`/`align`/`indent`/`line-suffix`/`if-break` if possible
|
|
function cleanDoc(doc) {
|
|
return mapDoc(doc, (currentDoc) => cleanDocFn(currentDoc));
|
|
}
|
|
|
|
function normalizeParts(parts) {
|
|
const newParts = [];
|
|
|
|
const restParts = parts.filter(Boolean);
|
|
while (restParts.length > 0) {
|
|
const part = restParts.shift();
|
|
|
|
if (!part) {
|
|
continue;
|
|
}
|
|
|
|
if (Array.isArray(part)) {
|
|
restParts.unshift(...part);
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
newParts.length > 0 &&
|
|
typeof newParts.at(-1) === "string" &&
|
|
typeof part === "string"
|
|
) {
|
|
newParts[newParts.length - 1] += part;
|
|
continue;
|
|
}
|
|
|
|
newParts.push(part);
|
|
}
|
|
|
|
return newParts;
|
|
}
|
|
|
|
function normalizeDoc(doc) {
|
|
return mapDoc(doc, (currentDoc) => {
|
|
if (Array.isArray(currentDoc)) {
|
|
return normalizeParts(currentDoc);
|
|
}
|
|
if (!currentDoc.parts) {
|
|
return currentDoc;
|
|
}
|
|
return {
|
|
...currentDoc,
|
|
parts: normalizeParts(currentDoc.parts),
|
|
};
|
|
});
|
|
}
|
|
|
|
function replaceEndOfLine(doc, replacement = literalline) {
|
|
return mapDoc(doc, (currentDoc) =>
|
|
typeof currentDoc === "string"
|
|
? join(replacement, currentDoc.split("\n"))
|
|
: currentDoc,
|
|
);
|
|
}
|
|
|
|
function canBreakFn(doc) {
|
|
if (doc.type === DOC_TYPE_LINE) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function canBreak(doc) {
|
|
return findInDoc(doc, canBreakFn, false);
|
|
}
|
|
|
|
function inheritLabel(doc, fn) {
|
|
return doc.type === DOC_TYPE_LABEL
|
|
? { ...doc, contents: fn(doc.contents) }
|
|
: fn(doc);
|
|
}
|
|
|
|
export {
|
|
getDocParts,
|
|
willBreak,
|
|
traverseDoc,
|
|
findInDoc,
|
|
mapDoc,
|
|
propagateBreaks,
|
|
removeLines,
|
|
stripTrailingHardline,
|
|
normalizeParts,
|
|
normalizeDoc,
|
|
cleanDoc,
|
|
replaceEndOfLine,
|
|
canBreak,
|
|
getDocType,
|
|
inheritLabel,
|
|
};
|