UNPKG

@mintlify/common

Version:

Commonly shared code within Mintlify

353 lines (352 loc) 12.4 kB
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';