storybook-multilevel-sort
Version:
Applies specific sort order to more than two levels of chapters and stories in a storybook.
164 lines (141 loc) • 6.63 kB
JavaScript
;
// See https://github.com/storybookjs/storybook/issues/548#issuecomment-1099949201
const hasKey = Object.hasOwn
? (obj, key) => Object.hasOwn(obj, key)
/* c8 ignore next */
: (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
const compareAlphabetical = (name1, name2) => name1.localeCompare(name2, { numeric: true });
const compareStoryPaths = (order, path1, path2, context) => {
/* c8 ignore next 9 */
if (path1.length === 0 && path2.length === 0) {
return 0
}
if (path1.length === 0 && path2.length > 0) {
// Path1 must be an ancestor of path2
return -1
}
if (path1.length > 0 && path2.length === 0) {
// Path2 must be an ancestor of path1
/* c8 ignore next 2 */
return 1
}
const [path1Head, ...path1Tail] = path1;
const [path2Head, ...path2Tail] = path2;
if (!order) {
// No reference order, so just sort alphabetically
const result = context.compareNames(path1Head, path2Head, { path1, path2 });
if (result === 0) {
return compareStoryPaths(null, path1Tail, path2Tail, context)
}
return result
}
let currentOrder;
const updateOrderAndCompare = (newOrder, path1, path2) => {
// Propagate the nested wildcard to the following call to compareStoryPaths
const nestedOrder = order['**'];
if (nestedOrder) context.order = nestedOrder;
currentOrder = newOrder;
// If the same paths are going to be compared, do not use the nested wildcard
// any more; it'd enter this clause once more and end up with a stack overflow
if (!currentOrder && context.path1 !== path1 && context.path2 !== path2) {
currentOrder = context.order;
}
// Remember the current paths for future calls to compareStoryPaths
context.path1 = path1;
context.path2 = path2;
return compareStoryPaths(currentOrder, path1, path2, context)
};
if (path1Head === path2Head) {
// The two paths share the same head; try either the key for the head, or the
// wildcard keys, otherwise pass `undefined` to sort without an explicit order
return updateOrderAndCompare(order[path1Head] || order['*'], path1Tail, path2Tail)
}
if (hasKey(order, path1Head) && hasKey(order, path2Head)) {
// If both heads are in the reference order, use the ordering of the keys in the reference order
const orderKeys = Object.keys(order);
return orderKeys.indexOf(path1Head) < orderKeys.indexOf(path2Head) ? -1 : 1
}
if (hasKey(order, path1Head) && !hasKey(order, path2Head)) {
return -1 // Give preference to path1, since it is included in the reference order
}
if (!hasKey(order, path1Head) && hasKey(order, path2Head)) {
return 1 // Give preference to path2, since it is included in the reference order
}
// No explicit order for the path heads was found, try the wildcard key,
// otherwise pass `undefined` to sort without an explicit order
return updateOrderAndCompare(order['*'], path1, path2)
};
const compareTypes = (type1, type2, { typeOrder = ['docs', 'story'] } = {}) => {
// No ordedring of types means the same weight for all page typed and using
// only alphabetical comparison of story names.
if (!typeOrder.length) return 0
let index1 = typeOrder.indexOf(type1);
let index2 = typeOrder.indexOf(type2);
// If some of the types does not have an explicitly specified order, try
// assigning it the order of the wildcard type (`*`).
if (index1 < 0 || index2 < 0) {
const wildcardIndex = typeOrder.indexOf('*');
if (wildcardIndex >= 0) {
if (index1 < 0) index1 = wildcardIndex;
if (index2 < 0) index2 = wildcardIndex;
}
}
if (index1 >= 0) {
// If both types have their indexes in the type-ordering array, apply
// the usual numeric comparison on the type indexes.
if (index2 >= 0) {
/* c8 ignore next */
return index1 === index2 ? 0 : index1 < index2 ? -1 : 1
}
// If just the first type was found, the first story should go to the front.
return -1
}
// If just the second type was found, the second story should go to the front.
if (index2 >= 0) {
return 1
}
// If no type as found, both stories will start with the same weight
// in the just following name comparison.
return 0
};
const compareStories = (storyOrder, story1, story2, { typeOrder, compareNames } = {}) => {
const { title: title1, name: name1, type: type1 } = story1;
const { title: title2, name: name2, type: type2 } = story2;
// If the types are different and are not considered the same in the comparison,
// they alone are enough to decide the order of the stories and this group
// stories of the same type together. Only if the two stories are in the same
// group. Ancestor group names should be still sorted alphabetically.
if (type1 !== type2 && title1 === title2) {
const result = compareTypes(type1, type2, { typeOrder });
if (result) return result
}
// Extract case-insensitive name paths from story titles. For example:
// { title: 'Elements/Button', name: 'Documentation' }
// => [ 'elements', 'button', 'documentation' ]
// { title: 'Elements/Link', name: 'Active' }
// => [ 'elements', 'link', 'active' ]
const story1Path = [...title1.split('/'), name1].map(key => key.toLowerCase());
const story2Path = [...title2.split('/'), name2].map(key => key.toLowerCase());
if (!compareNames) compareNames = compareAlphabetical;
return compareStoryPaths(storyOrder, story1Path, story2Path, { compareNames })
};
const storySort = (story1, story2) => {
// A global object with the configuration is set by `configureSort`.
const params = globalThis['storybook-multilevel-sort:params'];
if (!params) throw new Error('Missing storybook-multilevel-sort:params object. Forgot to call configureSort?')
const { storyOrder, typeOrder, compareNames } = params;
return compareStories(storyOrder, story1, story2, { typeOrder, compareNames })
};
const configureSort = ({ storyOrder, typeOrder, compareNames } = {}) => {
const params = globalThis['storybook-multilevel-sort:params'] ||
// biome-ignore lint/suspicious/noAssignInExpressions: this is easier to read
(globalThis['storybook-multilevel-sort:params'] = {});
if (storyOrder !== undefined) params.storyOrder = storyOrder;
if (typeOrder !== undefined) params.typeOrder = typeOrder;
if (compareNames !== undefined) params.compareNames = compareNames;
globalThis['storybook-multilevel-sort:storySort'] = storySort;
};
exports.compareStories = compareStories;
exports.configureSort = configureSort;
exports.storySort = storySort;
//# sourceMappingURL=index.cjs.map