@mintlify/common 1.0.1150 → 1.0.1151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,9 +12,10 @@ type MDXOptionsData = {
12
12
  tailwindSelectors?: string[];
13
13
  path?: string;
14
14
  };
15
- export declare const getMDXOptions: ({ data, remarkPlugins, rehypePlugins, mdxExtracts, allowedComponents, fragmentPlugins, }: {
15
+ export declare const getMDXOptions: ({ data, remarkPlugins, earlyRemarkPlugins, rehypePlugins, mdxExtracts, allowedComponents, fragmentPlugins, }: {
16
16
  data: MDXOptionsData;
17
17
  remarkPlugins?: PluggableList;
18
+ earlyRemarkPlugins?: PluggableList;
18
19
  rehypePlugins?: PluggableList;
19
20
  mdxExtracts?: MdxExtracts;
20
21
  allowedComponents?: string[];
@@ -13,7 +13,7 @@ const rehypeExtractors = (mdxExtracts, data) => {
13
13
  [rehypeMdxExtractEndpoint, data.pageMetadata, data.config, mdxExtracts],
14
14
  ];
15
15
  };
16
- export const getMDXOptions = ({ data, remarkPlugins = [], rehypePlugins = [], mdxExtracts, allowedComponents = [], fragmentPlugins, }) => {
16
+ export const getMDXOptions = ({ data, remarkPlugins = [], earlyRemarkPlugins = [], rehypePlugins = [], mdxExtracts, allowedComponents = [], fragmentPlugins, }) => {
17
17
  // `fragment`: the list is compiling the body of an `<MDX>` inside an expression. Page-level
18
18
  // extractors that hoist an element out of the tree (`<Panel>`, `<RequestExample>`,
19
19
  // `<ResponseExample>`) are skipped there so the element stays in its branch and renders inline
@@ -36,10 +36,11 @@ export const getMDXOptions = ({ data, remarkPlugins = [], rehypePlugins = [], md
36
36
  budget,
37
37
  },
38
38
  ],
39
+ remarkPrompt,
40
+ ...earlyRemarkPlugins,
39
41
  remarkMdxStyleStringToObject,
40
42
  [remarkResolveRelativeLinks, { path: data.path }],
41
43
  remarkParseTitleMarkdown,
42
- remarkPrompt,
43
44
  [remarkComponentIds, data.pageMetadata],
44
45
  remarkUnwrapJsxHeadings,
45
46
  remarkUnwrapInlineJsx,
@@ -1,4 +1,5 @@
1
1
  export * from './remark.js';
2
+ export * from './mentions/index.js';
2
3
  export * from './snippets/index.js';
3
4
  export * from './utils.js';
4
5
  export * from './plugins/index.js';
package/dist/mdx/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './remark.js';
2
+ export * from './mentions/index.js';
2
3
  export * from './snippets/index.js';
3
4
  export * from './utils.js';
4
5
  export * from './plugins/index.js';
@@ -0,0 +1 @@
1
+ export * from './userMentionToken.js';
@@ -0,0 +1 @@
1
+ export * from './userMentionToken.js';
@@ -0,0 +1,22 @@
1
+ export declare const USER_MENTION_ID_SOURCE = "[0-9a-fA-F]{24}";
2
+ export declare function userMentionTokenPattern({ inText }?: TokenScanOptions): RegExp;
3
+ export type TokenScanOptions = {
4
+ inText?: boolean;
5
+ };
6
+ export declare const UNKNOWN_USER_LABEL = "Unknown user";
7
+ export declare const MAX_USER_MENTION_IDS = 1000;
8
+ export declare const MENTION_TEXT_ATTRIBUTES: ReadonlySet<string>;
9
+ export declare function isMentionTextAttribute(name: string): boolean;
10
+ export type UserMentionNameResolver = (userId: string) => string | undefined;
11
+ export type UserMentionTokenMatch = {
12
+ start: number;
13
+ end: number;
14
+ userId: string;
15
+ };
16
+ export declare function isUserMentionId(value: string): boolean;
17
+ export declare function userMentionToken(userId: string): string;
18
+ export declare function findUserMentionTokens(text: string, options?: TokenScanOptions): UserMentionTokenMatch[];
19
+ export declare function hasUserMentionTokens(text: string, options?: TokenScanOptions): boolean;
20
+ export declare function extractUserMentionIds(text: string, options?: TokenScanOptions): string[];
21
+ export declare function flattenUserMentionTokens(text: string, resolve?: UserMentionNameResolver, options?: TokenScanOptions): string;
22
+ export declare function escapeMentionNameForSource(name: string): string;
@@ -0,0 +1,77 @@
1
+ export const USER_MENTION_ID_SOURCE = '[0-9a-fA-F]{24}';
2
+ const USER_MENTION_ID_EXACT = new RegExp(`^${USER_MENTION_ID_SOURCE}$`);
3
+ // Ingest re-stringifies pages and escapes the bracket, so in raw source `@\\[id]` is the same token.
4
+ export function userMentionTokenPattern({ inText = false } = {}) {
5
+ return new RegExp(`@${inText ? '' : '\\\\?'}\\[(${USER_MENTION_ID_SOURCE})\\]`, 'g');
6
+ }
7
+ export const UNKNOWN_USER_LABEL = 'Unknown user';
8
+ export const MAX_USER_MENTION_IDS = 1000;
9
+ // Component string attributes that render as text. HTML attributes (alt, src, href, id, className...) are left as written.
10
+ export const MENTION_TEXT_ATTRIBUTES = new Set([
11
+ 'title',
12
+ 'description',
13
+ 'label',
14
+ 'caption',
15
+ 'subtitle',
16
+ 'placeholder',
17
+ 'tip',
18
+ 'headline',
19
+ 'text',
20
+ 'cta',
21
+ 'children',
22
+ 'summary',
23
+ 'heading',
24
+ ]);
25
+ export function isMentionTextAttribute(name) {
26
+ return MENTION_TEXT_ATTRIBUTES.has(name);
27
+ }
28
+ export function isUserMentionId(value) {
29
+ return USER_MENTION_ID_EXACT.test(value);
30
+ }
31
+ export function userMentionToken(userId) {
32
+ return `@[${userId}]`;
33
+ }
34
+ export function findUserMentionTokens(text, options) {
35
+ const matches = [];
36
+ const pattern = userMentionTokenPattern(options);
37
+ for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
38
+ const userId = match[1];
39
+ if (userId === undefined)
40
+ continue;
41
+ matches.push({
42
+ start: match.index,
43
+ end: match.index + match[0].length,
44
+ userId: userId.toLowerCase(),
45
+ });
46
+ }
47
+ return matches;
48
+ }
49
+ export function hasUserMentionTokens(text, options) {
50
+ return userMentionTokenPattern(options).test(text);
51
+ }
52
+ export function extractUserMentionIds(text, options) {
53
+ const ids = new Set();
54
+ const pattern = userMentionTokenPattern(options);
55
+ for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
56
+ const userId = match[1];
57
+ if (userId === undefined)
58
+ continue;
59
+ ids.add(userId.toLowerCase());
60
+ if (ids.size === MAX_USER_MENTION_IDS)
61
+ break;
62
+ }
63
+ return [...ids];
64
+ }
65
+ export function flattenUserMentionTokens(text, resolve, options) {
66
+ return text.replace(userMentionTokenPattern(options), (_token, userId) => {
67
+ var _a;
68
+ const name = (_a = resolve === null || resolve === void 0 ? void 0 : resolve(userId.toLowerCase())) === null || _a === void 0 ? void 0 : _a.trim();
69
+ return name ? name : UNKNOWN_USER_LABEL;
70
+ });
71
+ }
72
+ export function escapeMentionNameForSource(name) {
73
+ return name
74
+ .replace(/\s*\n\s*/g, ' ')
75
+ .replaceAll('&', '&amp;')
76
+ .replace(/([\\`*_{}[\]<>~|$])/g, '\\$1');
77
+ }
@@ -29,4 +29,6 @@ export * from './remarkResolveRelativeLinks.js';
29
29
  export * from './remarkMapExplicitAnchors.js';
30
30
  export * from './remarkUnwrapInlineJsx.js';
31
31
  export * from './remarkUnwrapJsxHeadings.js';
32
+ export { TITLE_COMPONENTS } from './remarkTitleMarkdown.js';
33
+ export * from './remarkUserMentions.js';
32
34
  export * from './remarkVisibilityForMarkdown.js';
@@ -29,4 +29,6 @@ export * from './remarkResolveRelativeLinks.js';
29
29
  export * from './remarkMapExplicitAnchors.js';
30
30
  export * from './remarkUnwrapInlineJsx.js';
31
31
  export * from './remarkUnwrapJsxHeadings.js';
32
+ export { TITLE_COMPONENTS } from './remarkTitleMarkdown.js';
33
+ export * from './remarkUserMentions.js';
32
34
  export * from './remarkVisibilityForMarkdown.js';
@@ -1,4 +1,4 @@
1
1
  import type { Root } from 'mdast';
2
2
  export declare const remarkMdxRemoveUnknownJsx: ({ allowlist }?: {
3
3
  allowlist?: string[];
4
- }) => (tree: Root) => Root | import("mdast").Link | import("mdast").Delete | import("mdast").Code | import("mdast-util-mdx").MdxJsxFlowElement | import("mdast-util-mdx").MdxJsxTextElement | import("mdast").Blockquote | import("mdast").Break | import("mdast").Definition | import("mdast").Emphasis | import("mdast").FootnoteDefinition | import("mdast").FootnoteReference | import("mdast").Heading | import("mdast").Html | import("mdast").Image | import("mdast").ImageReference | import("mdast").InlineCode | import("mdast").LinkReference | import("mdast").List | import("mdast").ListItem | import("mdast").Paragraph | import("mdast").Strong | import("mdast").Table | import("mdast").TableCell | import("mdast").TableRow | import("mdast").Text | import("mdast").ThematicBreak | import("mdast").Yaml | import("mdast-util-math").InlineMath | import("mdast-util-math").Math | import("mdast-util-mdx-expression").MdxTextExpression | import("mdast-util-mdx-expression").MdxFlowExpression | import("mdast-util-mdxjs-esm").MdxjsEsm;
4
+ }) => (tree: Root) => Root | import("mdast").Link | import("mdast").Delete | import("mdast").Code | import("mdast").Text | import("mdast").Heading | import("mdast-util-mdx").MdxJsxFlowElement | import("mdast-util-mdx").MdxJsxTextElement | import("mdast").Blockquote | import("mdast").Break | import("mdast").Definition | import("mdast").Emphasis | import("mdast").FootnoteDefinition | import("mdast").FootnoteReference | import("mdast").Html | import("mdast").Image | import("mdast").ImageReference | import("mdast").InlineCode | import("mdast").LinkReference | import("mdast").List | import("mdast").ListItem | import("mdast").Paragraph | import("mdast").Strong | import("mdast").Table | import("mdast").TableCell | import("mdast").TableRow | import("mdast").ThematicBreak | import("mdast").Yaml | import("mdast-util-math").InlineMath | import("mdast-util-math").Math | import("mdast-util-mdx-expression").MdxTextExpression | import("mdast-util-mdx-expression").MdxFlowExpression | import("mdast-util-mdxjs-esm").MdxjsEsm;
@@ -1,3 +1,13 @@
1
1
  import type { Root } from 'mdast';
2
2
  import type { VFile } from 'vfile';
3
+ declare module 'mdast-util-mdx-jsx' {
4
+ interface MdxJsxAttributeData {
5
+ promptBodySource?: boolean;
6
+ }
7
+ }
8
+ export declare const isPromptBodySourceAttribute: (attribute: {
9
+ data?: {
10
+ promptBodySource?: boolean;
11
+ };
12
+ }) => boolean;
3
13
  export declare const remarkPrompt: () => (tree: Root, file: VFile) => void;
@@ -1,4 +1,5 @@
1
1
  import { visit } from 'unist-util-visit';
2
+ export const isPromptBodySourceAttribute = (attribute) => { var _a; return ((_a = attribute.data) === null || _a === void 0 ? void 0 : _a.promptBodySource) === true; };
2
3
  const dedent = (text) => {
3
4
  const lines = text.split('\n');
4
5
  const indents = lines
@@ -62,6 +63,7 @@ export const remarkPrompt = () => {
62
63
  node.attributes.push({
63
64
  type: 'mdxJsxAttribute',
64
65
  name: 'children',
66
+ data: { promptBodySource: true },
65
67
  value: {
66
68
  type: 'mdxJsxAttributeValueExpression',
67
69
  value: JSON.stringify(trimmedContent),
@@ -1,3 +1,4 @@
1
1
  import type { Root } from 'mdast';
2
+ export declare const TITLE_COMPONENTS: ReadonlySet<string>;
2
3
  export declare const remarkParseTitleMarkdown: () => (tree: Root) => void;
3
4
  export declare const remarkRenderTitleMarkdown: () => (tree: Root) => void;
@@ -3,7 +3,14 @@ import jsx from 'acorn-jsx';
3
3
  import { visit } from 'unist-util-visit';
4
4
  import { parseTitleMarkdown, titleMarkdownToText } from '../../titleMarkdown.js';
5
5
  const JSXParser = Parser.extend(jsx());
6
- const TITLE_COMPONENTS = new Set(['Accordion', 'Card', 'Color.Row', 'Step', 'Tab', 'Tile']);
6
+ export const TITLE_COMPONENTS = new Set([
7
+ 'Accordion',
8
+ 'Card',
9
+ 'Color.Row',
10
+ 'Step',
11
+ 'Tab',
12
+ 'Tile',
13
+ ]);
7
14
  const TITLE_DATA_KEY = 'mintlifyTitleMarkdown';
8
15
  function nodeJsx(node) {
9
16
  if (node.type === 'text')
@@ -0,0 +1,6 @@
1
+ import type { Root } from 'mdast';
2
+ import { type UserMentionNameResolver } from '../../mentions/userMentionToken.js';
3
+ export type RemarkUserMentionsOptions = {
4
+ resolveUserName?: UserMentionNameResolver;
5
+ };
6
+ export declare const remarkUserMentions: ({ resolveUserName }?: RemarkUserMentionsOptions) => (tree: Root) => void;
@@ -0,0 +1,107 @@
1
+ import { visit } from 'unist-util-visit';
2
+ import { escapeMentionNameForSource, flattenUserMentionTokens, hasUserMentionTokens, isMentionTextAttribute, isUserMentionId, } from '../../mentions/userMentionToken.js';
3
+ import { isPromptBodySourceAttribute } from './remarkPrompt.js';
4
+ import { TITLE_COMPONENTS } from './remarkTitleMarkdown.js';
5
+ const IN_TEXT = { inText: true };
6
+ const isMdxJsxElement = (node) => node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement';
7
+ const isParent = (node) => 'children' in node && Array.isArray(node.children);
8
+ const isRecord = (value) => typeof value === 'object' && value !== null;
9
+ function flattenStringLiteralExpression(expression, resolve, { escapeNames, rawSource }) {
10
+ var _a;
11
+ const program = (_a = expression.data) === null || _a === void 0 ? void 0 : _a.estree;
12
+ const statement = program === null || program === void 0 ? void 0 : program.body[0];
13
+ if (!statement || program.body.length !== 1 || statement.type !== 'ExpressionStatement')
14
+ return;
15
+ const literal = statement.expression;
16
+ if (!isRecord(literal) || literal.type !== 'Literal' || typeof literal.value !== 'string')
17
+ return;
18
+ const scan = rawSource ? undefined : IN_TEXT;
19
+ if (!hasUserMentionTokens(literal.value, scan))
20
+ return;
21
+ const flat = flattenUserMentionTokens(literal.value, (userId) => {
22
+ const name = resolve === null || resolve === void 0 ? void 0 : resolve(userId);
23
+ return name === undefined || !escapeNames ? name : escapeMentionNameForSource(name);
24
+ }, scan);
25
+ literal.value = flat;
26
+ literal.raw = JSON.stringify(flat);
27
+ expression.value = JSON.stringify(flat);
28
+ }
29
+ function foldShortcutReferences(parent, resolve) {
30
+ var _a;
31
+ const children = parent.children;
32
+ for (let index = 0; index < children.length - 1; index++) {
33
+ const text = children[index];
34
+ const reference = children[index + 1];
35
+ if (!text || !reference || text.type !== 'text' || !text.value.endsWith('@'))
36
+ continue;
37
+ if (reference.type !== 'linkReference' || reference.referenceType !== 'shortcut')
38
+ continue;
39
+ const label = (_a = reference.label) !== null && _a !== void 0 ? _a : reference.identifier;
40
+ if (!isUserMentionId(label))
41
+ continue;
42
+ const [only, ...rest] = reference.children;
43
+ if (rest.length > 0 || !only || only.type !== 'text' || only.value !== label)
44
+ continue;
45
+ text.value = text.value.slice(0, -1) + flattenUserMentionTokens(`@[${label}]`, resolve);
46
+ children.splice(index + 1, 1);
47
+ const next = children[index + 1];
48
+ if ((next === null || next === void 0 ? void 0 : next.type) === 'text') {
49
+ text.value += next.value;
50
+ children.splice(index + 1, 1);
51
+ }
52
+ index--;
53
+ }
54
+ }
55
+ // A Prompt body is plain text on the page, so code and math inside it are text there too.
56
+ function flattenPromptCode(prompt, resolve) {
57
+ visit(prompt, (child) => {
58
+ if (child.type !== 'inlineCode' &&
59
+ child.type !== 'code' &&
60
+ child.type !== 'inlineMath' &&
61
+ child.type !== 'math') {
62
+ return;
63
+ }
64
+ if (hasUserMentionTokens(child.value)) {
65
+ child.value = flattenUserMentionTokens(child.value, resolve);
66
+ }
67
+ });
68
+ }
69
+ export const remarkUserMentions = ({ resolveUserName } = {}) => (tree) => {
70
+ // Body text and text-bearing component attributes only: image alt/title, link and definition titles are HTML attributes and stay as written.
71
+ visit(tree, (node) => {
72
+ if (node.type === 'text') {
73
+ if (hasUserMentionTokens(node.value, IN_TEXT)) {
74
+ node.value = flattenUserMentionTokens(node.value, resolveUserName, IN_TEXT);
75
+ }
76
+ return;
77
+ }
78
+ if (isMdxJsxElement(node)) {
79
+ if (node.name === 'Prompt')
80
+ flattenPromptCode(node, resolveUserName);
81
+ const titleIsMarkdown = node.name !== null && TITLE_COMPONENTS.has(node.name);
82
+ for (const attribute of node.attributes) {
83
+ if (attribute.type !== 'mdxJsxAttribute' || !isMentionTextAttribute(attribute.name)) {
84
+ continue;
85
+ }
86
+ if (typeof attribute.value === 'string') {
87
+ if (!hasUserMentionTokens(attribute.value, IN_TEXT))
88
+ continue;
89
+ const escape = titleIsMarkdown && attribute.name === 'title';
90
+ attribute.value = flattenUserMentionTokens(attribute.value, (userId) => {
91
+ const name = resolveUserName === null || resolveUserName === void 0 ? void 0 : resolveUserName(userId);
92
+ return name === undefined || !escape ? name : escapeMentionNameForSource(name);
93
+ }, IN_TEXT);
94
+ }
95
+ else if (attribute.value && typeof attribute.value === 'object') {
96
+ flattenStringLiteralExpression(attribute.value, resolveUserName, {
97
+ escapeNames: titleIsMarkdown && attribute.name === 'title',
98
+ rawSource: isPromptBodySourceAttribute(attribute),
99
+ });
100
+ }
101
+ }
102
+ }
103
+ if (isParent(node) && node.children.some((child) => child.type === 'linkReference')) {
104
+ foldShortcutReferences(node, resolveUserName);
105
+ }
106
+ });
107
+ };
@@ -1,7 +1,9 @@
1
1
  import type { Root } from 'mdast';
2
2
  import type { PluggableList } from 'unified';
3
+ import { type RemarkUserMentionsOptions } from './plugins/remark/remarkUserMentions.js';
3
4
  export declare const coreRemarkMdxPlugins: PluggableList;
4
5
  export declare const coreRemark: import("unified").Processor<Root, undefined, undefined, Root, string>;
5
6
  export declare const getAST: (str: string, filePath?: string) => Root;
6
7
  export declare const stringifyTree: (tree: Root) => string;
7
8
  export declare function stripVisibilityForMarkdown(mdx: string): string;
9
+ export declare function transformContentForMarkdown(mdx: string, mentionOptions?: RemarkUserMentionsOptions): string;
@@ -4,6 +4,7 @@ import remarkGfm from 'remark-gfm';
4
4
  import remarkMath from 'remark-math';
5
5
  import remarkMdx from 'remark-mdx';
6
6
  import remarkStringify from 'remark-stringify';
7
+ import { remarkUserMentions, } from './plugins/remark/remarkUserMentions.js';
7
8
  import { remarkVisibilityForMarkdown } from './plugins/remark/remarkVisibilityForMarkdown.js';
8
9
  import { preprocessCustomHeadingIds } from './preprocessCustomHeadingIds.js';
9
10
  import { preprocessMdxFragmentCode } from './preprocessMdxFragmentCode.js';
@@ -28,3 +29,9 @@ export function stripVisibilityForMarkdown(mdx) {
28
29
  remarkVisibilityForMarkdown()(tree);
29
30
  return stringifyTree(tree);
30
31
  }
32
+ export function transformContentForMarkdown(mdx, mentionOptions = {}) {
33
+ const tree = getAST(mdx);
34
+ remarkVisibilityForMarkdown()(tree);
35
+ remarkUserMentions(mentionOptions)(tree);
36
+ return stringifyTree(tree);
37
+ }
@@ -13,7 +13,7 @@ export type GetMdxType = {
13
13
  panelMdxSourceWithNoJs?: SerializeSuccess;
14
14
  parseFailed?: boolean;
15
15
  };
16
- export declare function getMdx({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors, pageType, trace, customLanguages, variables, rehypePlugins, remarkPlugins, allowedComponents, }: {
16
+ export declare function getMdx({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors, pageType, trace, customLanguages, variables, rehypePlugins, remarkPlugins, earlyRemarkPlugins, allowedComponents, }: {
17
17
  path: string;
18
18
  content: string;
19
19
  metadata: PageMetaTags;
@@ -28,6 +28,7 @@ export declare function getMdx({ path, content, metadata, snippets, subdomain, c
28
28
  variables?: Record<string, string>;
29
29
  rehypePlugins?: PluggableList;
30
30
  remarkPlugins?: PluggableList;
31
+ earlyRemarkPlugins?: PluggableList;
31
32
  allowedComponents?: string[];
32
33
  }): Promise<GetMdxType>;
33
34
  export { createSnippetTreeMap, type Snippet };
@@ -17,7 +17,7 @@ import { replaceVariables } from '../replaceVariables.js';
17
17
  import { createSnippetTreeMap } from './getMdx/snippets.js';
18
18
  import { getSerializeExtras } from './getSerializeExtras.js';
19
19
  export function getMdx(_a) {
20
- return __awaiter(this, arguments, void 0, function* ({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors = undefined, pageType = 'default', trace = undefined, customLanguages = [], variables = undefined, rehypePlugins = [], remarkPlugins = [], allowedComponents = [], }) {
20
+ return __awaiter(this, arguments, void 0, function* ({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors = undefined, pageType = 'default', trace = undefined, customLanguages = [], variables = undefined, rehypePlugins = [], remarkPlugins = [], earlyRemarkPlugins = [], allowedComponents = [], }) {
21
21
  const traceFn = trace !== null && trace !== void 0 ? trace : ((_name, fn) => fn());
22
22
  const processedContent = preprocessCustomHeadingIds(preprocessMdxFragmentCode(replaceVariables(content, variables)));
23
23
  const processedSnippets = snippets.map((snippet) => (Object.assign(Object.assign({}, snippet), { content: preprocessCustomHeadingIds(preprocessMdxFragmentCode(replaceVariables(snippet.content, variables))) })));
@@ -49,6 +49,7 @@ export function getMdx(_a) {
49
49
  const mdxOptions = getMDXOptions({
50
50
  data: mdxOptionsData,
51
51
  remarkPlugins: plugins,
52
+ earlyRemarkPlugins,
52
53
  rehypePlugins,
53
54
  mdxExtracts,
54
55
  allowedComponents,
@@ -57,6 +58,7 @@ export function getMdx(_a) {
57
58
  const mdxOptionsNoJs = getMDXOptions({
58
59
  data: mdxOptionsData,
59
60
  remarkPlugins: [remarkMdxRemoveJs, ...plugins],
61
+ earlyRemarkPlugins,
60
62
  rehypePlugins,
61
63
  allowedComponents,
62
64
  fragmentPlugins: getSerializeExtras(syntaxHighlightingOptions),
@@ -1,2 +1,3 @@
1
1
  import type { SavedRssFileV4, RSSItemV2 } from '../types/rss.js';
2
+ export declare const feedSafeName: (name: string) => string;
2
3
  export declare const getV4FeedUpdates: (rssFile: SavedRssFileV4) => Promise<RSSItemV2[]>;
@@ -7,6 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import { remarkUserMentions } from '../mdx/plugins/remark/remarkUserMentions.js';
10
11
  import { getAST } from '../mdx/remark.js';
11
12
  import { UPDATE_MAX } from './constants.js';
12
13
  import { getDateForNode, getMostRepresentativeDate } from './dates.js';
@@ -14,8 +15,16 @@ import { enhanceSnippetsWithImportNames } from './enhanceSnippetsWithImportNames
14
15
  import { isUpdate } from './index.js';
15
16
  import { processUpdateNode } from './processUpdateNode.js';
16
17
  import { resolveSnippets } from './resolveSnippets.js';
18
+ const WORD_JOINER = '\u2060';
19
+ // GFM autolink literals are found by a text-node transform after parsing, so backslash escapes cannot stop `alice@example.com`, `www.x.com` or `https://x` in a name from becoming a link when the feed is re-parsed. An invisible word joiner inside each pattern does.
20
+ export const feedSafeName = (name) => name
21
+ .replace(/\s+/g, ' ')
22
+ .replace(/[<>]/g, '')
23
+ .replace(/@/g, `@${WORD_JOINER}`)
24
+ .replace(/:\/\//g, `:${WORD_JOINER}//`)
25
+ .replace(/\b(www)\./gi, `$1${WORD_JOINER}.`);
17
26
  export const getV4FeedUpdates = (rssFile) => __awaiter(void 0, void 0, void 0, function* () {
18
- const { content, lineBlame, snippets } = rssFile;
27
+ const { content, lineBlame, snippets, mentionUserNames } = rssFile;
19
28
  if (!content) {
20
29
  return [];
21
30
  }
@@ -36,6 +45,13 @@ export const getV4FeedUpdates = (rssFile) => __awaiter(void 0, void 0, void 0, f
36
45
  }
37
46
  catch (_a) { }
38
47
  }
48
+ // Feed content is re-parsed as HTML downstream (encodeContentAsHtml): inside an HTML block a name is raw markup, and a blank line would end the block and hand the rest to markdown.
49
+ remarkUserMentions({
50
+ resolveUserName: (userId) => {
51
+ const name = mentionUserNames === null || mentionUserNames === void 0 ? void 0 : mentionUserNames[userId];
52
+ return name === undefined ? undefined : feedSafeName(name);
53
+ },
54
+ })(ast);
39
55
  const updateComponents = ast.children.filter((child) => isUpdate(child));
40
56
  if (updateComponents.length === 0) {
41
57
  return [];
package/dist/rss/index.js CHANGED
@@ -166,32 +166,108 @@ export const getMarkdownHeadingProps = (heading) => {
166
166
  }
167
167
  return { title, anchor };
168
168
  };
169
- const jsxToHtml = (node) => {
169
+ const escapeHtmlText = (value) => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
170
+ const escapeHtmlAttribute = (value) => escapeHtmlText(value).replaceAll('"', '&quot;');
171
+ // CommonMark HTML block start condition 6: content of these tags is raw HTML when the feed is re-parsed; content of any other tag is inline HTML and the text between tags is still markdown.
172
+ // CommonMark HTML block start conditions 1 and 6: text inside these is raw HTML, never re-parsed as markdown.
173
+ const HTML_BLOCK_TAGS = new Set([
174
+ 'pre',
175
+ 'script',
176
+ 'style',
177
+ 'textarea',
178
+ 'address',
179
+ 'article',
180
+ 'aside',
181
+ 'base',
182
+ 'basefont',
183
+ 'blockquote',
184
+ 'body',
185
+ 'caption',
186
+ 'center',
187
+ 'col',
188
+ 'colgroup',
189
+ 'dd',
190
+ 'details',
191
+ 'dialog',
192
+ 'dir',
193
+ 'div',
194
+ 'dl',
195
+ 'dt',
196
+ 'fieldset',
197
+ 'figcaption',
198
+ 'figure',
199
+ 'footer',
200
+ 'form',
201
+ 'frame',
202
+ 'frameset',
203
+ 'h1',
204
+ 'h2',
205
+ 'h3',
206
+ 'h4',
207
+ 'h5',
208
+ 'h6',
209
+ 'head',
210
+ 'header',
211
+ 'hr',
212
+ 'html',
213
+ 'iframe',
214
+ 'legend',
215
+ 'li',
216
+ 'link',
217
+ 'main',
218
+ 'menu',
219
+ 'menuitem',
220
+ 'nav',
221
+ 'noframes',
222
+ 'ol',
223
+ 'optgroup',
224
+ 'option',
225
+ 'p',
226
+ 'param',
227
+ 'search',
228
+ 'section',
229
+ 'summary',
230
+ 'table',
231
+ 'tbody',
232
+ 'td',
233
+ 'tfoot',
234
+ 'th',
235
+ 'thead',
236
+ 'title',
237
+ 'tr',
238
+ 'track',
239
+ 'ul',
240
+ ]);
241
+ const escapeMarkdownText = (value) => value.replace(/([\\`*_{}[\]()#+!~|])/g, '\\$1');
242
+ const jsxToHtml = (node, insideHtmlBlock = HTML_BLOCK_TAGS.has((node.name || 'div').toLowerCase())) => {
170
243
  const tagName = node.name || 'div';
171
244
  const attrs = node.attributes
172
245
  .filter((attr) => attr.type === 'mdxJsxAttribute' && typeof attr.value === 'string')
173
246
  .map((attr) => {
174
247
  if (attr.type === 'mdxJsxAttribute' && attr.name !== 'className') {
175
- return `${attr.name}="${attr.value}"`;
248
+ return `${attr.name}="${escapeHtmlAttribute(String(attr.value))}"`;
176
249
  }
177
250
  return '';
178
251
  })
179
252
  .filter(Boolean)
180
253
  .join(' ');
181
254
  const attrsStr = attrs ? ` ${attrs}` : '';
182
- const childrenStr = node.children
183
- .map((child) => {
184
- if (child.type === 'mdxJsxFlowElement' || child.type === 'mdxJsxTextElement') {
185
- return jsxToHtml(child);
186
- }
187
- if (child.type === 'text') {
188
- return child.value;
189
- }
190
- return stringifyTree({ type: 'root', children: [child] }).trim();
191
- })
192
- .join('');
193
- return `<${tagName}${attrsStr}>${childrenStr}</${tagName}>`;
255
+ return `<${tagName}${attrsStr}>${childrenToHtml(node.children, insideHtmlBlock)}</${tagName}>`;
194
256
  };
257
+ const childrenToHtml = (children, insideHtmlBlock) => children
258
+ .map((child) => {
259
+ if (child.type === 'mdxJsxFlowElement' || child.type === 'mdxJsxTextElement') {
260
+ return jsxToHtml(child, insideHtmlBlock);
261
+ }
262
+ if (child.type === 'text') {
263
+ return escapeHtmlText(insideHtmlBlock ? child.value : escapeMarkdownText(child.value));
264
+ }
265
+ if (insideHtmlBlock && child.type === 'paragraph') {
266
+ return childrenToHtml(child.children, insideHtmlBlock);
267
+ }
268
+ return stringifyTree({ type: 'root', children: [child] }).trim();
269
+ })
270
+ .join('');
195
271
  const stripCustomInlineJsx = (parent) => {
196
272
  let i = 0;
197
273
  while (i < parent.children.length) {
@@ -208,6 +284,15 @@ const stripCustomInlineJsx = (parent) => {
208
284
  i++;
209
285
  }
210
286
  };
287
+ // `<pre>text</pre>` on one line parses as a paragraph around an inline element, but CommonMark still reads that line as an HTML block.
288
+ const soleBlockJsx = (child) => {
289
+ if (child.type !== 'paragraph' || child.children.length !== 1)
290
+ return undefined;
291
+ const only = child.children[0];
292
+ if ((only === null || only === void 0 ? void 0 : only.type) !== 'mdxJsxTextElement')
293
+ return undefined;
294
+ return HTML_BLOCK_TAGS.has((only.name || '').toLowerCase()) ? only : undefined;
295
+ };
211
296
  const stringifyTreeForRSS = (tree) => {
212
297
  stripCustomInlineJsx(tree);
213
298
  return tree.children
@@ -215,6 +300,9 @@ const stringifyTreeForRSS = (tree) => {
215
300
  if (child.type === 'mdxJsxFlowElement' || child.type === 'mdxJsxTextElement') {
216
301
  return jsxToHtml(child);
217
302
  }
303
+ const block = soleBlockJsx(child);
304
+ if (block)
305
+ return jsxToHtml(block, true);
218
306
  return stringifyTree({ type: 'root', children: [child] }).trim();
219
307
  })
220
308
  .join('\n\n');