@terrafirmagreg-team/mdx 1.2.4 → 1.2.5

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.
@@ -0,0 +1,102 @@
1
+ import {
2
+ contentChildren,
3
+ extractDirectiveLabel,
4
+ extractLeafLabel,
5
+ estreeBoolean,
6
+ estreeString,
7
+ jsxAttr,
8
+ jsxExpression,
9
+ walkDirectives,
10
+ type DirectiveNode,
11
+ type JsxAttribute,
12
+ type MdastNode,
13
+ } from '../directive-utils.ts';
14
+
15
+ export type EntrySpec = {
16
+ directive: string;
17
+ component: string;
18
+ leafKey?: string;
19
+ labelAttr?: string;
20
+ usePagePack?: boolean;
21
+ transform?: (ctx: {
22
+ directive: DirectiveNode;
23
+ attrs: JsxAttribute[];
24
+ children: MdastNode[];
25
+ isLeaf: boolean;
26
+ file: { path?: string };
27
+ }) => MdastNode[] | void;
28
+ };
29
+
30
+ function pageDataset(file: { path?: string }): string | undefined {
31
+ return file.path
32
+ ?.replaceAll('\\', '/')
33
+ .match(/content\/docs\/([^/]+)\/[^/]+\//)?.[1];
34
+ }
35
+
36
+ export function toAttributes(directive: DirectiveNode): JsxAttribute[] {
37
+ return Object.entries(directive.attributes ?? {}).map(([name, value]) => ({
38
+ type: 'mdxJsxAttribute' as const,
39
+ name,
40
+ value:
41
+ value == null || value === ''
42
+ ? jsxExpression(estreeBoolean(true))
43
+ : typeof value === 'string'
44
+ ? value
45
+ : jsxExpression(estreeString(String(value))),
46
+ }));
47
+ }
48
+
49
+ export function createEntryRemark(spec: EntrySpec) {
50
+ const {
51
+ directive: name,
52
+ component,
53
+ leafKey = 'id',
54
+ labelAttr = 'label',
55
+ usePagePack = false,
56
+ transform,
57
+ } = spec;
58
+
59
+ return function remark() {
60
+ return (tree: MdastNode, file: { path?: string }) => {
61
+ walkDirectives(tree, (directive) => {
62
+ if (directive.name !== name) return;
63
+ if (
64
+ directive.type !== 'leafDirective' &&
65
+ directive.type !== 'containerDirective'
66
+ )
67
+ return;
68
+
69
+ const attrs = toAttributes(directive);
70
+ const pack = usePagePack ? pageDataset(file) : undefined;
71
+ if (
72
+ pack &&
73
+ directive.attributes?.pack == null &&
74
+ directive.attributes?.defaultPack == null
75
+ ) {
76
+ attrs.push(jsxAttr('defaultPack', pack));
77
+ }
78
+
79
+ const isLeaf = directive.type === 'leafDirective';
80
+ const label = (
81
+ isLeaf
82
+ ? extractLeafLabel(directive)
83
+ : extractDirectiveLabel(directive)
84
+ ).trim();
85
+ const key = isLeaf ? leafKey : labelAttr;
86
+ if (label && key) attrs.unshift(jsxAttr(key, label));
87
+
88
+ const children = isLeaf ? [] : contentChildren(directive);
89
+ const out =
90
+ transform?.({ directive, attrs, children, isLeaf, file }) ??
91
+ children;
92
+
93
+ Object.assign(directive, {
94
+ type: 'mdxJsxFlowElement',
95
+ name: component,
96
+ attributes: attrs,
97
+ children: out,
98
+ });
99
+ });
100
+ };
101
+ };
102
+ }
@@ -0,0 +1,6 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkFluidEntry = createEntryRemark({
4
+ directive: 'fluid',
5
+ component: 'FluidEntry',
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkGuideEntry = createEntryRemark({
4
+ directive: 'guide',
5
+ component: 'GuideEntry',
6
+ });
@@ -0,0 +1,9 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkImageEntry = createEntryRemark({
4
+ directive: 'image',
5
+ component: 'DocsImage',
6
+ leafKey: 'src',
7
+ labelAttr: 'alt',
8
+ usePagePack: true,
9
+ });
@@ -0,0 +1,45 @@
1
+ import type { MdastNode } from '../directive-utils.ts';
2
+ import { remarkBlockEntry } from './block.ts';
3
+ import { remarkEntityEntry } from './entity.ts';
4
+ import { remarkFluidEntry } from './fluid.ts';
5
+ import { remarkGuideEntry } from './guide.ts';
6
+ import { remarkImageEntry } from './image.ts';
7
+ import { remarkItemEntry } from './item.ts';
8
+ import { remarkMultiblockEntry } from './multiblock.ts';
9
+ import { remarkQuestEntry } from './quest.ts';
10
+ import { remarkRecipeEntry } from './recipe.ts';
11
+ import { remarkStoryEntry } from './story.ts';
12
+ import { remarkTagEntry } from './tag.ts';
13
+
14
+ export { remarkBlockEntry } from './block.ts';
15
+ export { remarkEntityEntry } from './entity.ts';
16
+ export { remarkFluidEntry } from './fluid.ts';
17
+ export { remarkGuideEntry } from './guide.ts';
18
+ export { remarkImageEntry } from './image.ts';
19
+ export { remarkItemEntry } from './item.ts';
20
+ export { remarkMultiblockEntry } from './multiblock.ts';
21
+ export { remarkQuestEntry } from './quest.ts';
22
+ export { remarkRecipeEntry } from './recipe.ts';
23
+ export { remarkStoryEntry } from './story.ts';
24
+ export { remarkTagEntry } from './tag.ts';
25
+
26
+ const ALL = [
27
+ remarkItemEntry,
28
+ remarkBlockEntry,
29
+ remarkFluidEntry,
30
+ remarkEntityEntry,
31
+ remarkTagEntry,
32
+ remarkQuestEntry,
33
+ remarkGuideEntry,
34
+ remarkMultiblockEntry,
35
+ remarkImageEntry,
36
+ remarkStoryEntry,
37
+ remarkRecipeEntry,
38
+ ];
39
+
40
+ export function remarkEntries() {
41
+ const plugins = ALL.map((factory) => factory());
42
+ return (tree: MdastNode, file: { path?: string }) => {
43
+ for (const plugin of plugins) plugin(tree, file);
44
+ };
45
+ }
@@ -0,0 +1,6 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkItemEntry = createEntryRemark({
4
+ directive: 'item',
5
+ component: 'ItemEntry',
6
+ });
@@ -0,0 +1,16 @@
1
+ import { jsxAttr, type MdastNode } from '../directive-utils.ts';
2
+ import { createEntryRemark } from './factory.ts';
3
+
4
+ export const remarkMultiblockEntry = createEntryRemark({
5
+ directive: 'multiblock',
6
+ component: 'Multiblock',
7
+ transform({ directive, attrs, isLeaf }) {
8
+ if (isLeaf) return;
9
+ const code = directive.children?.find(
10
+ (child) => child.type === 'code',
11
+ ) as { value?: string } | undefined;
12
+ if (!code?.value) return;
13
+ attrs.unshift(jsxAttr('spec', code.value));
14
+ return [] as MdastNode[];
15
+ },
16
+ });
@@ -0,0 +1,6 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkQuestEntry = createEntryRemark({
4
+ directive: 'quest',
5
+ component: 'QuestEntry',
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkRecipeEntry = createEntryRemark({
4
+ directive: 'recipe',
5
+ component: 'RecipeEntry',
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkStoryEntry = createEntryRemark({
4
+ directive: 'story',
5
+ component: 'Story', leafKey: 'name',
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createEntryRemark } from './factory.ts';
2
+
3
+ export const remarkTagEntry = createEntryRemark({
4
+ directive: 'tag',
5
+ component: 'TagEntry',
6
+ });
@@ -0,0 +1,129 @@
1
+ import type {
2
+ FileNode,
3
+ FolderNode,
4
+ ToMdxOptions,
5
+ } from 'fumadocs-core/mdx-plugins/remark-mdx-files';
6
+ import { toString } from 'mdast-util-to-string';
7
+ import { visit } from 'unist-util-visit';
8
+ import type { DirectiveNode } from './directive-utils.ts';
9
+ import type { JsxAttribute, JsxElement, MdastNode } from '../../types/mdx';
10
+
11
+ type TreeNode = FileNode | FolderNode;
12
+
13
+ const LINK_RE = /^\[([^\]]+)\]\((\S+?)(?:\s+"([^"]*)")?\)$/;
14
+
15
+ type TreeName = { name: string; href?: string; description?: string };
16
+
17
+ function parseName(raw: string): TreeName {
18
+ const match = LINK_RE.exec(raw.trim());
19
+ if (!match) return { name: raw };
20
+ const [, text, href, description] = match;
21
+ const out: TreeName = { name: text, href };
22
+ if (description) out.description = description;
23
+ return out;
24
+ }
25
+
26
+ function attributes(parsed: TreeName): JsxAttribute[] {
27
+ const attrs: JsxAttribute[] = [
28
+ { type: 'mdxJsxAttribute', name: 'name', value: parsed.name },
29
+ ];
30
+ if (parsed.href)
31
+ attrs.push({ type: 'mdxJsxAttribute', name: 'href', value: parsed.href });
32
+ if (parsed.description)
33
+ attrs.push({
34
+ type: 'mdxJsxAttribute',
35
+ name: 'description',
36
+ value: parsed.description,
37
+ });
38
+ return attrs;
39
+ }
40
+
41
+ function nodeToMdx(node: TreeNode, options: ToMdxOptions): JsxElement {
42
+ const parsed = parseName(node.name);
43
+ const attrs = attributes(parsed);
44
+ const isFolder = node.type === 'folder' || parsed.name.endsWith('/');
45
+ if (!isFolder)
46
+ return {
47
+ type: 'mdxJsxFlowElement',
48
+ name: 'File',
49
+ attributes: attrs,
50
+ children: [],
51
+ };
52
+ if (options.defaultOpenAll)
53
+ attrs.push({ type: 'mdxJsxAttribute', name: 'defaultOpen', value: true });
54
+ const children =
55
+ node.type === 'folder'
56
+ ? node.children.map((item) => nodeToMdx(item, options) as unknown as MdastNode)
57
+ : [];
58
+ return {
59
+ type: 'mdxJsxFlowElement',
60
+ name: 'Folder',
61
+ attributes: attrs,
62
+ children,
63
+ };
64
+ }
65
+
66
+ export function filesToMdx(node: TreeNode, options: ToMdxOptions): JsxElement {
67
+ return {
68
+ type: 'mdxJsxFlowElement',
69
+ name: 'Files',
70
+ attributes: [],
71
+ children:
72
+ node.type === 'folder' && node.name === ''
73
+ ? node.children.map((item) => nodeToMdx(item, options) as unknown as MdastNode)
74
+ : [nodeToMdx(node, options) as unknown as MdastNode],
75
+ };
76
+ }
77
+
78
+ type ListNode = MdastNode & { children?: MdastNode[] };
79
+
80
+ function itemToTree(item: ListNode): TreeNode {
81
+ const paragraph = item.children?.find((c) => c.type === 'paragraph');
82
+ const nested = item.children?.find((c) => c.type === 'list');
83
+ const link =
84
+ paragraph?.children?.length === 1 &&
85
+ paragraph.children[0].type === 'link'
86
+ ? (paragraph.children[0] as unknown as {
87
+ url: string;
88
+ title?: string | null;
89
+ children: { value: string }[];
90
+ })
91
+ : undefined;
92
+ let name = link ? toString(link.children[0] as never) : toString(paragraph as never);
93
+ const href = link?.url;
94
+ const description = link?.title ?? undefined;
95
+ const annotated = href ? `[${name}](${href}${description ? ` "${description}"` : ''})` : name;
96
+ const isFolder = !!nested || name.endsWith('/');
97
+ if (isFolder) name = name.replace(/\/+$/, '') || name;
98
+ if (!isFolder) return { type: 'file', name: annotated } as FileNode;
99
+ const children = (nested?.children ?? []).map(
100
+ (child) => itemToTree(child as ListNode) as TreeNode,
101
+ );
102
+ return {
103
+ type: 'folder',
104
+ name: href ? `[${name}/](${href}${description ? ` "${description}"` : ''})` : name,
105
+ children,
106
+ } as FolderNode;
107
+ }
108
+
109
+ export function remarkFilesDirective() {
110
+ return (tree: MdastNode) => {
111
+ visit(tree as never, 'containerDirective', (node) => {
112
+ const directive = node as DirectiveNode;
113
+ if (directive.name !== 'files') return;
114
+ const children = (directive.children ?? []).flatMap((child) =>
115
+ child.type === 'list' ? (child.children ?? []) : [],
116
+ );
117
+ if (!children.length) return;
118
+ const root: FolderNode = {
119
+ type: 'folder',
120
+ name: '',
121
+ children: children.map((item) => itemToTree(item as ListNode)),
122
+ } as FolderNode;
123
+ Object.assign(
124
+ directive,
125
+ filesToMdx(root, { defaultOpenAll: true }),
126
+ );
127
+ });
128
+ };
129
+ }
@@ -18,12 +18,15 @@ const SKIP = new Set([
18
18
  'html',
19
19
  ]);
20
20
 
21
- const CHANGELOG_PATH = /[/\\]changelog[/\\]([^/\\]+)[/\\]/;
21
+ const DEFAULT_PATH = /[/\\]changelog[/\\]([^/\\]+)[/\\]/;
22
22
 
23
- export function remarkChangelogReferences(options: { datasetConfig?: Record<string, { repo?: string }> } = {}) {
24
- const datasetConfig = options.datasetConfig ?? {};
23
+ export function remarkGithubAutolinks(options: {
24
+ datasetConfig?: Record<string, { repo?: string }>;
25
+ path?: RegExp;
26
+ } = {}) {
27
+ const { datasetConfig = {}, path = DEFAULT_PATH } = options;
25
28
  return (tree: MdastNode, file: { path?: string }) => {
26
- const pack = CHANGELOG_PATH.exec(file.path ?? '')?.[1];
29
+ const pack = path.exec(file.path ?? '')?.[1];
27
30
  const repo = pack ? datasetConfig[pack]?.repo : undefined;
28
31
  if (!repo) return;
29
32
  const repoUrl = `https://github.com/${repo}`;
@@ -54,4 +57,3 @@ export function remarkChangelogReferences(options: { datasetConfig?: Record<stri
54
57
  visit(tree);
55
58
  };
56
59
  }
57
-
@@ -14,7 +14,6 @@ export function remarkTabs() {
14
14
  walkContainers(tree, (directive) => {
15
15
  if (directive.name !== 'tabs') return;
16
16
  toTabs(directive);
17
- return false;
18
17
  });
19
18
  };
20
19
  }
@@ -12,6 +12,7 @@ export type DirectiveMeta = {
12
12
  multiline?: string[];
13
13
  props?: string[];
14
14
  solo?: boolean;
15
+ paired?: boolean;
15
16
  descriptions?: Record<string, string>;
16
17
  raw?: string;
17
18
  variants?: Record<string, Partial<DirectiveMeta>>;
@@ -89,30 +90,25 @@ const DIRECTIVE_META: Record<string, DirectiveMeta> = {
89
90
  strokeWidth: 'Lucide stroke width, default 2',
90
91
  },
91
92
  },
92
- 'gradient-text': {
93
- name: 'gradient',
94
- label: 'children',
95
- colon: 1,
96
- props: ['from', 'to', 'image', 'children', 'className'],
93
+ gradient: {
94
+ colon: 2,
95
+ paired: true,
96
+ multiline: ['children'],
97
+ props: ['from', 'to', 'dir', 'image', 'children', 'className'],
97
98
  descriptions: {
98
99
  from: 'Gradient start color',
99
100
  to: 'Gradient end color',
101
+ dir: 'CSS gradient direction (to right, 135deg, …)',
100
102
  image: 'CSS image overriding the gradient',
101
- children: 'Text content, the directive label',
103
+ children: 'Body markdown inside the ::gradient … :: block',
102
104
  },
103
105
  },
104
- 'glow-header': {
105
- name: 'glow',
106
- label: 'children',
107
- colon: 1,
108
- props: ['from', 'to', 'image', 'gradient', 'fade', 'children', 'className'],
106
+ video: {
107
+ colon: 2,
108
+ props: ['src', 'title', 'className'],
109
109
  descriptions: {
110
- from: 'Gradient start color',
111
- to: 'Gradient end color',
112
- image: 'CSS image overriding the gradient',
113
- gradient: 'Also tint the background box with the gradient',
114
- fade: 'Gradient border along the bottom edge',
115
- children: 'Header text, the directive label',
110
+ src: 'Embed URL (e.g. youtube-nocookie embed)',
111
+ title: 'Accessible frame title',
116
112
  },
117
113
  },
118
114
  callout: {
@@ -126,11 +122,20 @@ const DIRECTIVE_META: Record<string, DirectiveMeta> = {
126
122
  title: 'Overrides the default localized title',
127
123
  children: 'Body markdown',
128
124
  },
125
+ variants: {
126
+ Alert: {
127
+ raw: '> [!TIP]\nA GitHub alert blockquote.',
128
+ },
129
+ },
129
130
  },
130
131
  banner: {
131
132
  colon: 3,
132
133
  multiline: ['children'],
133
- props: ['id', 'height', 'variant', 'rainbowColors', 'changeLayout', 'children', 'className'],
134
+ props: ['id', 'height', 'variant', 'gradient', 'from', 'to', 'dir', 'image', 'rainbowColors', 'changeLayout', 'children', 'className'],
135
+ descriptions: {
136
+ variant: 'normal | rainbow | fade (gradient ring, fades down) | glow (radial bg + shadow)',
137
+ gradient: 'Gradient-fill the title text',
138
+ },
134
139
  },
135
140
  tabs: {
136
141
  colon: 3,
@@ -147,7 +152,25 @@ const DIRECTIVE_META: Record<string, DirectiveMeta> = {
147
152
  },
148
153
  accordions: {
149
154
  colon: 3,
150
- props: ['multiple', 'defaultValue', 'children', 'className'],
155
+ props: ['format', 'multiple', 'defaultValue', 'children', 'className'],
156
+ descriptions: {
157
+ format:
158
+ 'Input format — details: single :::details[Title] block; accordions: :::accordions group with ::accordion[Title] items; jsx: raw <Accordions>/<Accordion> markup',
159
+ },
160
+ variants: {
161
+ Details: {
162
+ raw: ':::details[Summary]\nHidden content.\n:::',
163
+ },
164
+ Open: {
165
+ raw: ':::details[Summary]{open}\nHidden content.\n:::',
166
+ },
167
+ 'Multiple': {
168
+ raw: ':::accordions{multiple}\n::accordion[First]\nContent one.\n::accordion[Second]\nContent two.\n:::',
169
+ },
170
+ 'Nested': {
171
+ raw: '::::details[Outer]{open}\nBefore.\n:::accordions\n::accordion[Inner one]\nNested answer one.\n::accordion[Inner two]\nNested answer two.\n:::\n::::',
172
+ },
173
+ },
151
174
  },
152
175
  image: {
153
176
  label: 'alt',
@@ -171,7 +194,33 @@ const DIRECTIVE_META: Record<string, DirectiveMeta> = {
171
194
  ├── mods
172
195
  └── config
173
196
  `,
174
- props: ['children'],
197
+ props: ['format', 'children'],
198
+ descriptions: {
199
+ format:
200
+ 'Input format — list: :::files markdown list; fenced: ```files code block',
201
+ name: 'Entry name — file or folder label',
202
+ icon: 'Custom icon name or path',
203
+ href: 'Link target — renders the entry as an anchor',
204
+ description: 'Muted description line under the entry',
205
+ disabled: 'Folder only — renders the entry as non-interactive',
206
+ defaultOpen: 'Folder only — starts expanded',
207
+ children:
208
+ 'Tree body — a ```files code fence or a :::files markdown list; items: [name](href "description"), trailing / or a nested list = folder',
209
+ },
210
+ variants: {
211
+ List: {
212
+ raw: ':::files\n- app/\n - layout.tsx\n - page.tsx\n- package.json\n:::',
213
+ },
214
+ Links: {
215
+ raw: ':::files\n- [modpack/](/docs/modern/modpack "Modpack guides")\n - [optional-mods.md](/docs/modern/modpack/optional-mods "Extra compatible mods")\n - kubejs/\n - server_scripts\n - startup_scripts\n- [pakku.json](https://pakku.readthedocs.io "Pakku config")\n:::',
216
+ },
217
+ Deep: {
218
+ raw: ':::files\n- content/\n - docs/\n - dev/\n - components/\n - index.md\n - item.md\n - meta.json\n - meta.json\n - index.mdx\n:::',
219
+ },
220
+ Fenced: {
221
+ raw: '```files\nTerraFirmaGreg\n├── mods\n└── config\n```',
222
+ },
223
+ },
175
224
  },
176
225
  code: {
177
226
  fence: 'ts',
@@ -322,12 +371,30 @@ const DIRECTIVE_META: Record<string, DirectiveMeta> = {
322
371
  },
323
372
  },
324
373
  math: {
325
- body: ':math[e = mc^2]',
326
- props: ['children', 'block'],
374
+ props: ['format', 'children', 'block'],
327
375
  multiline: ['children'],
328
376
  descriptions: {
329
- children: 'KaTeX source, the `:math[…]` label',
330
- block: 'Display block (`::math`/`:::math`) instead of inline `:math`',
377
+ format:
378
+ 'Input syntax — inline: :math[…] or $…$; block: ::math[…] leaf, :::math container, ```math fence or $$…$$',
379
+ children: 'KaTeX source',
380
+ block: 'Display block instead of inline',
381
+ },
382
+ variants: {
383
+ Inline: { raw: ':math[e = mc^2]' },
384
+ Dollar: { raw: '$e = mc^2$' },
385
+ Leaf: {
386
+ raw: '::math[\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}]',
387
+ },
388
+ Block: {
389
+ raw: ':::math\n\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}\n:::',
390
+ },
391
+ Fenced: {
392
+ raw: '```math\n\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}\n```',
393
+ },
394
+ Aligned: {
395
+ raw: ':::math\n\\begin{aligned}\n\\nabla \\cdot \\vec{E} &= \\frac{\\rho}{\\varepsilon_0} \\\\\n\\nabla \\times \\vec{B} &= \\mu_0 \\vec{J}\n\\end{aligned}\n:::',
396
+ },
397
+ Display: { raw: '$$e = mc^2$$' },
331
398
  },
332
399
  },
333
400
  };
@@ -401,11 +468,10 @@ export function remarkFor(
401
468
  if (key === 'accordions') {
402
469
  return md`
403
470
  :::accordions${args.multiple ? '{multiple}' : ''}
404
- ::accordion[Title one]
405
- …
406
-
407
- ::accordion[Title two]
408
- …
471
+ ::accordion[First]
472
+ Content one.
473
+ ::accordion[Second]
474
+ Content two.
409
475
  :::
410
476
  `;
411
477
  }
@@ -460,9 +526,9 @@ export function remarkFor(
460
526
  ? `[${String(args[meta.label])}]`
461
527
  : '';
462
528
  const head = `${':'.repeat(meta.colon ?? 2)}${name}${label}${attrs ? `{${attrs}}` : ''}`;
463
- if (meta.colon !== 3 || meta.solo) return head;
529
+ if (meta.solo || (meta.colon !== 3 && !meta.paired)) return head;
464
530
  const inner =
465
531
  typeof args.children === 'string' && args.children ? args.children : '…';
466
- return `${head}\n${inner}\n:::`;
532
+ return `${head}\n${inner}\n${':'.repeat(meta.colon ?? 3)}`;
467
533
  }
468
534