@mintlify/common
Version:
Commonly shared code within Mintlify
353 lines (352 loc) • 12.4 kB
JavaScript
import slugify from '@sindresorhus/slugify';
import { stringifyTree } from '../mdx/index.js';
import { getArrayExpressionStringProperties, getObjectExpressionStringProperty, } from '../mdx/utils.js';
export const isFrontmatter = (node) => {
return (node === null || node === void 0 ? void 0 : node.type) === 'yaml';
};
export const isUpdate = (node) => {
return (node === null || node === void 0 ? void 0 : node.type) === 'mdxJsxFlowElement' && node.name === 'Update';
};
export const isHeading = (node) => {
if (!node) {
return false;
}
return node.type === 'heading';
};
// Standard HTML tags that should be included in RSS feeds
const STANDARD_HTML_TAGS = new Set([
'table',
'tr',
'td',
'th',
'thead',
'tbody',
'tfoot',
'p',
'div',
'span',
'a',
'br',
'hr',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'ul',
'ol',
'li',
'strong',
'em',
'b',
'i',
'u',
'img',
'figure',
'figcaption',
'blockquote',
'pre',
'code',
]);
export const isNormalMarkdown = (node) => {
if (!node) {
return false;
}
if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
return STANDARD_HTML_TAGS.has(node.name || '');
}
return node.type !== 'code' && node.type !== 'inlineCode';
};
export const containsUpdates = (tree) => {
return tree.children.some((child) => isUpdate(child));
};
export const getTags = (node) => {
let tags = [];
const tagsAttribute = node.attributes.find((attr) => 'name' in attr && attr.name === 'tags');
if (!tagsAttribute || !tagsAttribute.value || typeof tagsAttribute.value !== 'object') {
return tags;
}
try {
tags = JSON.parse(tagsAttribute.value.value);
}
catch (_a) {
tags = getArrayExpressionStringProperties(tagsAttribute);
}
if (tags.length > 0) {
return tags;
}
return undefined;
};
export const getRssPropsData = (updateComponent) => {
const attributes = updateComponent.attributes;
const rssData = attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'rss');
const title = getObjectExpressionStringProperty('title', rssData);
const description = getObjectExpressionStringProperty('description', rssData);
return { rssTitle: title, rssDescription: description };
};
export const getUpdateTitle = (updateComponent) => {
var _a;
const attributes = updateComponent.attributes;
const label = (_a = attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'label')) === null || _a === void 0 ? void 0 : _a.value;
if (label) {
return label.toString();
}
return undefined;
};
export const getUpdateDescription = (updateComponent) => {
var _a;
const attributes = updateComponent.attributes;
const descriptionAttribute = (_a = attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'description')) === null || _a === void 0 ? void 0 : _a.value;
if (descriptionAttribute) {
return descriptionAttribute.toString();
}
return undefined;
};
export const compareUpdates = ({ newTree, previousTree, }) => {
const newUpdateComponents = newTree.children.filter((child) => isUpdate(child));
const previousUpdateComponents = previousTree.children
.filter((child) => isUpdate(child))
.map(getUpdateTitle);
const previousUpdateComponentsSet = new Set(previousUpdateComponents);
const newUpdates = newUpdateComponents.filter((component) => {
const title = getUpdateTitle(component);
return !previousUpdateComponentsSet.has(title);
});
return newUpdates;
};
export const matchRSSTitle = (node, title) => {
const { rssTitle } = getRssPropsData(node);
const label = getUpdateTitle(node);
const nodeTitle = label || rssTitle;
return nodeTitle === title;
};
export const splitChildrenAtHeadings = (children) => {
return children.reduce((acc, child) => {
if (isHeading(child)) {
acc.push([child]);
}
else {
if (isNormalMarkdown(child)) {
if (acc.length === 0) {
acc.push([child]);
}
else {
const lastGroup = acc[acc.length - 1];
if (lastGroup) {
lastGroup.push(child);
}
}
}
}
return acc;
}, []);
};
export const getMarkdownHeadingProps = (heading) => {
const headingContent = heading.children[0];
let title = undefined;
let anchor = undefined;
if ((headingContent === null || headingContent === void 0 ? void 0 : headingContent.type) === 'text') {
title = headingContent.value;
}
if (title) {
anchor = slugify(title);
}
return { title, anchor };
};
const jsxToHtml = (node) => {
const tagName = node.name || 'div';
const attrs = node.attributes
.filter((attr) => attr.type === 'mdxJsxAttribute' && typeof attr.value === 'string')
.map((attr) => {
if (attr.type === 'mdxJsxAttribute' && attr.name !== 'className') {
return `${attr.name}="${attr.value}"`;
}
return '';
})
.filter(Boolean)
.join(' ');
const attrsStr = attrs ? ` ${attrs}` : '';
const childrenStr = node.children
.map((child) => {
if (child.type === 'mdxJsxFlowElement' || child.type === 'mdxJsxTextElement') {
return jsxToHtml(child);
}
if (child.type === 'text') {
return child.value;
}
return stringifyTree({ type: 'root', children: [child] }).trim();
})
.join('');
return `<${tagName}${attrsStr}>${childrenStr}</${tagName}>`;
};
const stripCustomInlineJsx = (parent) => {
let i = 0;
while (i < parent.children.length) {
const child = parent.children[i];
if (child.type === 'mdxJsxTextElement' &&
!STANDARD_HTML_TAGS.has(child.name || '')) {
const jsxChildren = child.children;
parent.children.splice(i, 1, ...jsxChildren);
continue;
}
if ('children' in child) {
stripCustomInlineJsx(child);
}
i++;
}
};
const stringifyTreeForRSS = (tree) => {
stripCustomInlineJsx(tree);
return tree.children
.map((child) => {
if (child.type === 'mdxJsxFlowElement' || child.type === 'mdxJsxTextElement') {
return jsxToHtml(child);
}
return stringifyTree({ type: 'root', children: [child] }).trim();
})
.join('\n\n');
};
export const updateGroupToRSSItemV2 = ({ group, date, }) => {
const dateToUse = date || new Date().toISOString();
const heading = group[0];
if (!heading || !isHeading(heading)) {
return undefined;
}
const { title, anchor } = getMarkdownHeadingProps(heading);
const content = group.slice(1);
const contentString = stringifyTreeForRSS({
type: 'root',
children: content,
});
if (!title || !anchor) {
return undefined;
}
return { title, date: dateToUse, anchor, content: contentString };
};
export const getNewContent = (newUpdateComponents) => {
const newUpdates = [];
for (const component of newUpdateComponents) {
const children = component.children;
const updatesByHeading = splitChildrenAtHeadings(children);
for (const group of updatesByHeading) {
const newUpdate = updateGroupToRSSItemV2({ group });
if (newUpdate) {
newUpdates.push(newUpdate);
}
}
}
return newUpdates;
};
export const getNewMarkdownUpdates = ({ newTree, previousTree, previousUpdates, }) => {
const firstUpdateInNewTree = newTree.children.find(isUpdate);
const firstUpdateInPreviousTree = previousTree.children.find(isUpdate);
if (!isUpdate(firstUpdateInNewTree) || !isUpdate(firstUpdateInPreviousTree)) {
return [];
}
const firstUpdateTitleInNewTree = getUpdateTitle(firstUpdateInNewTree);
const firstUpdateTitleInPreviousTree = getUpdateTitle(firstUpdateInPreviousTree);
if (firstUpdateTitleInNewTree !== firstUpdateTitleInPreviousTree) {
return [];
}
const newUpdates = splitChildrenAtHeadings(firstUpdateInNewTree.children);
const actuallyNewUpdates = [];
for (const group of newUpdates) {
const newUpdate = updateGroupToRSSItemV2({ group });
if (newUpdate && !previousUpdates.find((update) => update.title === newUpdate.title)) {
actuallyNewUpdates.push(newUpdate);
}
}
return actuallyNewUpdates;
};
export const containsRssProps = (updateNode) => {
const attributes = updateNode.attributes;
const rssData = attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'rss');
return !!rssData;
};
export const containsMarkdownGroups = (updateNode) => {
var _a, _b;
const children = updateNode.children;
const updatesByMarkdown = splitChildrenAtHeadings(children);
return updatesByMarkdown.length > 0 && ((_b = (_a = updatesByMarkdown[0]) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.type) === 'heading';
};
export const containsMarkdownHeading = (updateNode, title) => {
const children = updateNode.children;
const containsHeading = children.some((child) => child.type === 'heading' &&
child.children.some((child) => child.type === 'text' && child.value.includes(title)));
return containsHeading;
};
// soon to be deprecated, use processUpdateNode.ts instead
export const processUpdateNode = ({ updateNode, date, }) => {
const updates = [];
const nowOrOldDate = date || new Date().toISOString();
const containsRss = containsRssProps(updateNode);
const containsMarkdown = containsMarkdownGroups(updateNode);
if (containsRss) {
const { rssTitle, rssDescription } = getRssPropsData(updateNode);
const newUpdates = processUpdatePerNode({
updateNode,
date: nowOrOldDate,
title: rssTitle,
description: rssDescription,
});
updates.push(...newUpdates);
}
else if (containsMarkdown) {
const updatesByMarkdown = splitChildrenAtHeadings(updateNode.children);
const newUpdates = processUpdatePerMarkdownGroup({
updateNode,
date: nowOrOldDate,
updatesByMarkdown,
});
updates.push(...newUpdates);
}
else {
const newUpdates = processUpdatePerNode({ updateNode, date: nowOrOldDate });
updates.push(...newUpdates);
}
return updates;
};
export const processUpdatePerNode = ({ updateNode, date, title, description, }) => {
const label = getUpdateTitle(updateNode);
if (!label) {
return [];
}
const rssTitle = title || label;
const anchor = slugify(label);
const categories = getTags(updateNode);
const contentNodes = updateNode.children.filter((child) => isNormalMarkdown(child));
const contentString = stringifyTreeForRSS({
type: 'root',
children: contentNodes,
});
return [
{
title: rssTitle,
date,
anchor,
description,
categories,
content: contentString,
},
];
};
export const processUpdatePerMarkdownGroup = ({ updateNode, date, updatesByMarkdown, }) => {
const updates = [];
const categories = getTags(updateNode);
for (const group of updatesByMarkdown) {
const newUpdate = updateGroupToRSSItemV2({ group, date });
if (newUpdate) {
updates.push(Object.assign(Object.assign({}, newUpdate), { categories }));
}
}
return updates;
};
export const isRSSFileV4 = (rssFile) => {
return rssFile.version === 'v4';
};
export { enhanceSnippetsWithImportNames } from './enhanceSnippetsWithImportNames.js';
export { getV4FeedUpdates } from './getV4FeedUpdates.js';
export { resolveSnippets } from './resolveSnippets.js';
export { encodeContentAsHtml } from './encodeContentAsHtml.js';
export { UPDATE_MAX } from './constants.js';