diff --git a/apps/svelte.dev/scripts/sync-docs/index.ts b/apps/svelte.dev/scripts/sync-docs/index.ts
index 9af5b20eb3..53f08ca5f7 100644
--- a/apps/svelte.dev/scripts/sync-docs/index.ts
+++ b/apps/svelte.dev/scripts/sync-docs/index.ts
@@ -1,4 +1,4 @@
-import { replace_export_type_placeholders, type Modules } from '@sveltejs/site-kit/markdown';
+import { preprocess } from '@sveltejs/site-kit/markdown/preprocess';
import path from 'node:path';
import { cpSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import ts from 'typescript';
@@ -6,6 +6,7 @@ import glob from 'tiny-glob/sync';
import { fileURLToPath } from 'node:url';
import { clone_repo, migrate_meta_json, replace_strings, strip_origin } from './utils';
import { get_types, read_d_ts_file, read_types } from './types';
+import type { Modules } from '@sveltejs/site-kit/markdown';
interface Package {
name: string;
@@ -149,7 +150,7 @@ for (const pkg of packages) {
const files = glob(`${DOCS}/${pkg.name}/**/*.md`);
for (const file of files) {
- const content = await replace_export_type_placeholders(readFileSync(file, 'utf-8'), modules);
+ const content = await preprocess(readFileSync(file, 'utf-8'), modules);
writeFileSync(file, content);
}
diff --git a/packages/site-kit/package.json b/packages/site-kit/package.json
index 50d147daee..bdbb93280b 100644
--- a/packages/site-kit/package.json
+++ b/packages/site-kit/package.json
@@ -77,8 +77,10 @@
"svelte": "./src/lib/docs/index.ts"
},
"./markdown": {
- "default": "./src/lib/markdown/index.ts",
- "svelte": "./src/lib/markdown/index.ts"
+ "default": "./src/lib/markdown/index.ts"
+ },
+ "./markdown/preprocess": {
+ "default": "./src/lib/markdown/preprocess/index.ts"
},
"./nav": {
"default": "./src/lib/nav/index.ts",
diff --git a/packages/site-kit/src/lib/markdown/index.ts b/packages/site-kit/src/lib/markdown/index.ts
index 2ace78f1a2..4170c129f4 100644
--- a/packages/site-kit/src/lib/markdown/index.ts
+++ b/packages/site-kit/src/lib/markdown/index.ts
@@ -1,7 +1,4 @@
-export {
- render_content_markdown as renderContentMarkdown,
- replace_export_type_placeholders
-} from './renderer';
+export { render_content_markdown as renderContentMarkdown } from './renderer';
export {
extract_frontmatter as extractFrontmatter,
diff --git a/packages/site-kit/src/lib/markdown/preprocess.ts b/packages/site-kit/src/lib/markdown/preprocess.ts
new file mode 100644
index 0000000000..b63f971c2f
--- /dev/null
+++ b/packages/site-kit/src/lib/markdown/preprocess.ts
@@ -0,0 +1,324 @@
+import { SHIKI_LANGUAGE_MAP } from './utils';
+import type { Declaration, TypeElement, Modules } from './index';
+
+/**
+ * Replace module/export placeholders during `sync-docs`
+ */
+export async function preprocess(content: string, modules: Modules) {
+ const REGEXES = {
+ /** Render a specific type from a module with more details. Example: `> EXPANDED_TYPES: svelte#compile` */
+ EXPANDED_TYPES: /> EXPANDED_TYPES: (.+?)#(.+)$/gm,
+ /** Render types from a specific module. Example: `> TYPES: svelte` */
+ TYPES: /> TYPES: (.+?)(?:#(.+))?$/gm,
+ /** Render all exports and types from a specific module. Example: `> MODULE: svelte` */
+ MODULE: /> MODULE: (.+?)$/gm,
+ /** Render the snippet of a specific export. Example: `> EXPORT_SNIPPET: svelte#compile` */
+ EXPORT_SNIPPET: /> EXPORT_SNIPPET: (.+?)#(.+)?$/gm,
+ /** Render all modules. Example: `> MODULES` */
+ MODULES: /> MODULES/g, //! /g is VERY IMPORTANT, OR WILL CAUSE INFINITE LOOP
+ /** Render all value exports from a specific module. Example: `> EXPORTS: svelte` */
+ EXPORTS: /> EXPORTS: (.+)/
+ };
+
+ if (REGEXES.EXPORTS.test(content)) {
+ throw new Error('yes');
+ }
+
+ if (!modules || modules.length === 0) {
+ return content
+ .replace(REGEXES.EXPANDED_TYPES, '')
+ .replace(REGEXES.TYPES, '')
+ .replace(REGEXES.EXPORT_SNIPPET, '')
+ .replace(REGEXES.MODULES, '')
+ .replace(REGEXES.EXPORTS, '');
+ }
+ content = await async_replace(content, REGEXES.EXPANDED_TYPES, async ([_, name, id]) => {
+ const module = modules.find((module) => module.name === name);
+ if (!module) throw new Error(`Could not find module ${name}`);
+ if (!module.types) return '';
+
+ const type = module.types.find((t) => t.name === id);
+
+ if (!type) throw new Error(`Could not find type ${name}#${id}`);
+
+ return stringify_expanded_type(type);
+ });
+
+ content = await async_replace(content, REGEXES.TYPES, async ([_, name, id]) => {
+ const module = modules.find((module) => module.name === name);
+ if (!module) throw new Error(`Could not find module ${name}`);
+ if (!module.types) return '';
+
+ if (id) {
+ const type = module.types.find((t) => t.name === id);
+
+ if (!type) throw new Error(`Could not find type ${name}#${id}`);
+
+ return render_declaration(type, true);
+ }
+
+ let comment = '';
+ if (module.comment) {
+ comment += `${module.comment}\n\n`;
+ }
+
+ return (
+ comment + module.types.map((t) => `## ${t.name}\n\n${render_declaration(t, true)}`).join('')
+ );
+ });
+
+ content = await async_replace(content, REGEXES.EXPORT_SNIPPET, async ([_, name, id]) => {
+ const module = modules.find((module) => module.name === name);
+ if (!module) throw new Error(`Could not find module ${name} for EXPORT_SNIPPET clause`);
+
+ if (!id) {
+ throw new Error(`id is required for module ${name}`);
+ }
+
+ const exported = module.exports?.filter((t) => t.name === id);
+
+ return exported?.map((d) => render_declaration(d, false)).join('\n\n') ?? '';
+ });
+
+ content = await async_replace(content, REGEXES.MODULE, async ([_, name]) => {
+ const module = modules.find((module) => module.name === name);
+ if (!module) throw new Error(`Could not find module ${name}`);
+
+ return stringify_module(module);
+ });
+
+ content = await async_replace(content, REGEXES.MODULES, async () => {
+ return modules
+ .map((module) => {
+ if (!module.exports) return;
+
+ if (module.exports.length === 0 && !module.exempt) return '';
+
+ let import_block = '';
+
+ if (module.exports.length > 0) {
+ // deduplication is necessary for now, because of `error()` overload
+ const exports = Array.from(new Set(module.exports?.map((x) => x.name)));
+
+ let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
+ if (declaration.length > 80) {
+ declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
+ }
+
+ import_block = fence(declaration, 'js');
+ }
+
+ return `## ${module.name}\n\n${import_block}\n\n${module.comment}\n\n${module.exports
+ .map((declaration) => {
+ const markdown = render_declaration(declaration, true);
+ return `### ${declaration.name}\n\n${markdown}`;
+ })
+ .join('\n\n')}`;
+ })
+ .join('\n\n');
+ });
+
+ content = await async_replace(content, REGEXES.EXPORTS, async ([_, name]) => {
+ const module = modules.find((module) => module.name === name);
+ if (!module) throw new Error(`Could not find module ${name} for EXPORTS: clause`);
+ if (!module.exports) return '';
+
+ if (module.exports.length === 0 && !module.exempt) return '';
+
+ let import_block = '';
+
+ if (module.exports.length > 0) {
+ // deduplication is necessary for now, because of `error()` overload
+ const exports = Array.from(new Set(module.exports.map((x) => x.name)));
+
+ let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
+ if (declaration.length > 80) {
+ declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
+ }
+
+ import_block = fence(declaration, 'js');
+ }
+
+ return `${import_block}\n\n${module.comment}\n\n${module.exports
+ .map((declaration) => {
+ const markdown = render_declaration(declaration, true);
+ return `### ${declaration.name}\n\n${markdown}`;
+ })
+ .join('\n\n')}`;
+ });
+
+ return content;
+}
+
+function render_declaration(declaration: Declaration, full: boolean) {
+ let content = '';
+
+ if (declaration.deprecated) {
+ content += `
\n\n${declaration.deprecated}\n\n
\n\n`;
+ }
+
+ if (declaration.comment) {
+ content += declaration.comment + '\n\n';
+ }
+
+ return (
+ content +
+ declaration.overloads
+ .map((overload) => {
+ const children = full
+ ? overload.children?.map((val) => stringify(val, 'dts')).join('\n\n')
+ : '';
+
+ return `${fence(overload.snippet, 'dts')}${children}
\n\n`;
+ })
+ .join('')
+ );
+}
+
+async function async_replace(
+ inputString: string,
+ regex: RegExp,
+ asyncCallback: (match: RegExpExecArray) => string | Promise
+) {
+ let match;
+ let previousLastIndex = 0;
+ let parts = [];
+
+ // While there is a match
+ while ((match = regex.exec(inputString)) !== null) {
+ // Add the text before the match
+ parts.push(inputString.slice(previousLastIndex, match.index));
+
+ // Perform the asynchronous operation for the match and add the result
+ parts.push(await asyncCallback(match));
+
+ // Update the previous last index
+ previousLastIndex = regex.lastIndex;
+
+ // Avoid infinite loops with zero-width matches
+ if (match.index === regex.lastIndex) {
+ regex.lastIndex++;
+ }
+ }
+
+ // Add the remaining text
+ parts.push(inputString.slice(previousLastIndex));
+
+ return parts.join('');
+}
+
+/**
+ * Takes a module and returns a markdown string.
+ */
+function stringify_module(module: Modules[0]) {
+ let content = '';
+
+ if (module.exports && module.exports.length > 0) {
+ // deduplication is necessary for now, because of method overloads
+ const exports = Array.from(new Set(module.exports?.map((x) => x.name)));
+
+ let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
+ if (declaration.length > 80) {
+ declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
+ }
+
+ content += fence(declaration, 'js');
+ }
+
+ if (module.comment) {
+ content += `${module.comment}\n\n`;
+ }
+
+ for (const declaration of module.exports || []) {
+ const markdown = render_declaration(declaration, true);
+ content += `## ${declaration.name}\n\n${markdown}\n\n`;
+ }
+
+ for (const t of module.types || []) {
+ content += `## ${t.name}\n\n` + render_declaration(t, true);
+ }
+
+ return content;
+}
+
+function stringify_expanded_type(type: Declaration) {
+ return (
+ type.comment +
+ type.overloads
+ .map((overload) =>
+ overload.children
+ ?.map((child) => {
+ let section = `## ${child.name}`;
+
+ if (child.bullets) {
+ section += `\n\n\n\n${child.bullets.join(
+ '\n'
+ )}\n\n
`;
+ }
+
+ section += `\n\n${child.comment}`;
+
+ if (child.children) {
+ section += `\n\n\n\n${child.children
+ .map((v) => stringify(v))
+ .join('\n')}\n\n
`;
+ }
+
+ return section;
+ })
+ .join('\n\n')
+ )
+ .join('\n\n')
+ );
+}
+
+/**
+ * Helper function for {@link replace_export_type_placeholders}. Renders specifiv members to their markdown/html representation.
+ */
+function stringify(member: TypeElement, lang: keyof typeof SHIKI_LANGUAGE_MAP = 'ts'): string {
+ if (!member) return '';
+
+ // It's important to always use two newlines after a dom tag or else markdown does not render it properly
+
+ const bullet_block =
+ (member.bullets?.length ?? 0) > 0
+ ? `\n\n\n\n${member.bullets?.join('\n')}\n\n
`
+ : '';
+
+ const comment = member.comment
+ ? '\n\n' +
+ member.comment
+ .replace(/\/\/\/ type: (.+)/g, '/** @type {$1} */')
+ .replace(/^( )+/gm, (match, spaces) => {
+ return '\t'.repeat(match.length / 2);
+ })
+ : '';
+
+ const child_block =
+ (member.children?.length ?? 0) > 0
+ ? `\n\n${member.children
+ ?.map((val) => stringify(val, lang))
+ .join('\n')}
`
+ : '';
+
+ return (
+ `${fence(member.snippet, lang)}` +
+ `
` +
+ bullet_block +
+ comment +
+ child_block +
+ (bullet_block || comment || child_block ? '\n\n' : '') +
+ '
\n
'
+ );
+}
+
+function fence(code: string, lang: keyof typeof SHIKI_LANGUAGE_MAP = 'ts') {
+ return (
+ '\n\n```' +
+ lang +
+ '\n' +
+ (['js', 'ts'].includes(lang) ? '// @noErrors\n' : '') +
+ code +
+ '\n```\n\n'
+ );
+}
diff --git a/packages/site-kit/src/lib/markdown/renderer.ts b/packages/site-kit/src/lib/markdown/renderer.ts
index 208cc8709d..03663f62ec 100644
--- a/packages/site-kit/src/lib/markdown/renderer.ts
+++ b/packages/site-kit/src/lib/markdown/renderer.ts
@@ -7,7 +7,7 @@ import * as prettier from 'prettier';
import { codeToHtml, createCssVariablesTheme } from 'shiki';
import { transformerTwoslash } from '@shikijs/twoslash';
import { SHIKI_LANGUAGE_MAP, escape, normalizeSlugify, smart_quotes, transform } from './utils';
-import type { Declaration, TypeElement, Modules } from './index';
+import type { Modules } from './index';
interface SnippetOptions {
file: string | null;
@@ -136,8 +136,6 @@ export async function render_content_markdown(
const { type_links, type_regex } = create_type_links(modules, resolveTypeLinks);
const snippets = await create_snippet_cache(cacheCodeSnippets);
- body = await replace_export_type_placeholders(body, modules);
-
const headings: string[] = [];
// this is a bit hacky, but it allows us to prevent type declarations
@@ -499,296 +497,6 @@ export async function convert_to_ts(js_code: string, indent = '', offset = '') {
}
}
-/**
- * Replace module/export information placeholders in the docs.
- */
-export async function replace_export_type_placeholders(content: string, modules: Modules) {
- const REGEXES = {
- /** Render a specific type from a module with more details. Example: `> EXPANDED_TYPES: svelte#compile` */
- EXPANDED_TYPES: /> EXPANDED_TYPES: (.+?)#(.+)$/gm,
- /** Render types from a specific module. Example: `> TYPES: svelte` */
- TYPES: /> TYPES: (.+?)(?:#(.+))?$/gm,
- /** Render all exports and types from a specific module. Example: `> MODULE: svelte` */
- MODULE: /> MODULE: (.+?)$/gm,
- /** Render the snippet of a specific export. Example: `> EXPORT_SNIPPET: svelte#compile` */
- EXPORT_SNIPPET: /> EXPORT_SNIPPET: (.+?)#(.+)?$/gm,
- /** Render all modules. Example: `> MODULES` */
- MODULES: /> MODULES/g, //! /g is VERY IMPORTANT, OR WILL CAUSE INFINITE LOOP
- /** Render all value exports from a specific module. Example: `> EXPORTS: svelte` */
- EXPORTS: /> EXPORTS: (.+)/
- };
-
- if (REGEXES.EXPORTS.test(content)) {
- throw new Error('yes');
- }
-
- if (!modules || modules.length === 0) {
- return content
- .replace(REGEXES.EXPANDED_TYPES, '')
- .replace(REGEXES.TYPES, '')
- .replace(REGEXES.EXPORT_SNIPPET, '')
- .replace(REGEXES.MODULES, '')
- .replace(REGEXES.EXPORTS, '');
- }
- content = await async_replace(content, REGEXES.EXPANDED_TYPES, async ([_, name, id]) => {
- const module = modules.find((module) => module.name === name);
- if (!module) throw new Error(`Could not find module ${name}`);
- if (!module.types) return '';
-
- const type = module.types.find((t) => t.name === id);
-
- if (!type) throw new Error(`Could not find type ${name}#${id}`);
-
- return stringify_expanded_type(type);
- });
-
- content = await async_replace(content, REGEXES.TYPES, async ([_, name, id]) => {
- const module = modules.find((module) => module.name === name);
- if (!module) throw new Error(`Could not find module ${name}`);
- if (!module.types) return '';
-
- if (id) {
- const type = module.types.find((t) => t.name === id);
-
- if (!type) throw new Error(`Could not find type ${name}#${id}`);
-
- return render_declaration(type, true);
- }
-
- let comment = '';
- if (module.comment) {
- comment += `${module.comment}\n\n`;
- }
-
- return (
- comment + module.types.map((t) => `## ${t.name}\n\n${render_declaration(t, true)}`).join('')
- );
- });
-
- content = await async_replace(content, REGEXES.EXPORT_SNIPPET, async ([_, name, id]) => {
- const module = modules.find((module) => module.name === name);
- if (!module) throw new Error(`Could not find module ${name} for EXPORT_SNIPPET clause`);
-
- if (!id) {
- throw new Error(`id is required for module ${name}`);
- }
-
- const exported = module.exports?.filter((t) => t.name === id);
-
- return exported?.map((d) => render_declaration(d, false)).join('\n\n') ?? '';
- });
-
- content = await async_replace(content, REGEXES.MODULE, async ([_, name]) => {
- const module = modules.find((module) => module.name === name);
- if (!module) throw new Error(`Could not find module ${name}`);
-
- return stringify_module(module);
- });
-
- content = await async_replace(content, REGEXES.MODULES, async () => {
- return modules
- .map((module) => {
- if (!module.exports) return;
-
- if (module.exports.length === 0 && !module.exempt) return '';
-
- let import_block = '';
-
- if (module.exports.length > 0) {
- // deduplication is necessary for now, because of `error()` overload
- const exports = Array.from(new Set(module.exports?.map((x) => x.name)));
-
- let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
- if (declaration.length > 80) {
- declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
- }
-
- import_block = fence(declaration, 'js');
- }
-
- return `## ${module.name}\n\n${import_block}\n\n${module.comment}\n\n${module.exports
- .map((declaration) => {
- const markdown = render_declaration(declaration, true);
- return `### ${declaration.name}\n\n${markdown}`;
- })
- .join('\n\n')}`;
- })
- .join('\n\n');
- });
-
- content = await async_replace(content, REGEXES.EXPORTS, async ([_, name]) => {
- const module = modules.find((module) => module.name === name);
- if (!module) throw new Error(`Could not find module ${name} for EXPORTS: clause`);
- if (!module.exports) return '';
-
- if (module.exports.length === 0 && !module.exempt) return '';
-
- let import_block = '';
-
- if (module.exports.length > 0) {
- // deduplication is necessary for now, because of `error()` overload
- const exports = Array.from(new Set(module.exports.map((x) => x.name)));
-
- let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
- if (declaration.length > 80) {
- declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
- }
-
- import_block = fence(declaration, 'js');
- }
-
- return `${import_block}\n\n${module.comment}\n\n${module.exports
- .map((declaration) => {
- const markdown = render_declaration(declaration, true);
- return `### ${declaration.name}\n\n${markdown}`;
- })
- .join('\n\n')}`;
- });
-
- return content;
-}
-
-function render_declaration(declaration: Declaration, full: boolean) {
- let content = '';
-
- if (declaration.deprecated) {
- content += `\n\n${declaration.deprecated}\n\n
\n\n`;
- }
-
- if (declaration.comment) {
- content += declaration.comment + '\n\n';
- }
-
- return (
- content +
- declaration.overloads
- .map((overload) => {
- const children = full
- ? overload.children?.map((val) => stringify(val, 'dts')).join('\n\n')
- : '';
-
- return `${fence(overload.snippet, 'dts')}${children}
\n\n`;
- })
- .join('')
- );
-}
-
-/**
- * Takes a module and returns a markdown string.
- */
-function stringify_module(module: Modules[0]) {
- let content = '';
-
- if (module.exports && module.exports.length > 0) {
- // deduplication is necessary for now, because of method overloads
- const exports = Array.from(new Set(module.exports?.map((x) => x.name)));
-
- let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
- if (declaration.length > 80) {
- declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
- }
-
- content += fence(declaration, 'js');
- }
-
- if (module.comment) {
- content += `${module.comment}\n\n`;
- }
-
- for (const declaration of module.exports || []) {
- const markdown = render_declaration(declaration, true);
- content += `## ${declaration.name}\n\n${markdown}\n\n`;
- }
-
- for (const t of module.types || []) {
- content += `## ${t.name}\n\n` + render_declaration(t, true);
- }
-
- return content;
-}
-
-function stringify_expanded_type(type: Declaration) {
- return (
- type.comment +
- type.overloads
- .map((overload) =>
- overload.children
- ?.map((child) => {
- let section = `## ${child.name}`;
-
- if (child.bullets) {
- section += `\n\n\n\n${child.bullets.join(
- '\n'
- )}\n\n
`;
- }
-
- section += `\n\n${child.comment}`;
-
- if (child.children) {
- section += `\n\n\n\n${child.children
- .map((v) => stringify(v))
- .join('\n')}\n\n
`;
- }
-
- return section;
- })
- .join('\n\n')
- )
- .join('\n\n')
- );
-}
-
-function fence(code: string, lang: keyof typeof SHIKI_LANGUAGE_MAP = 'ts') {
- return (
- '\n\n```' +
- lang +
- '\n' +
- (['js', 'ts'].includes(lang) ? '// @noErrors\n' : '') +
- code +
- '\n```\n\n'
- );
-}
-
-/**
- * Helper function for {@link replace_export_type_placeholders}. Renders specifiv members to their markdown/html representation.
- */
-function stringify(member: TypeElement, lang: keyof typeof SHIKI_LANGUAGE_MAP = 'ts'): string {
- if (!member) return '';
-
- // It's important to always use two newlines after a dom tag or else markdown does not render it properly
-
- const bullet_block =
- (member.bullets?.length ?? 0) > 0
- ? `\n\n\n\n${member.bullets?.join('\n')}\n\n
`
- : '';
-
- const comment = member.comment
- ? '\n\n' +
- member.comment
- .replace(/\/\/\/ type: (.+)/g, '/** @type {$1} */')
- .replace(/^( )+/gm, (match, spaces) => {
- return '\t'.repeat(match.length / 2);
- })
- : '';
-
- const child_block =
- (member.children?.length ?? 0) > 0
- ? `\n\n${member.children
- ?.map((val) => stringify(val, lang))
- .join('\n')}
`
- : '';
-
- return (
- `${fence(member.snippet, lang)}` +
- `
` +
- bullet_block +
- comment +
- child_block +
- (bullet_block || comment || child_block ? '\n\n' : '') +
- '
\n
'
- );
-}
-
function find_nearest_node_modules(file: string): string | null {
let current = file;
@@ -1036,35 +744,3 @@ function indent_multiline_comments(str: string) {
}
);
}
-
-async function async_replace(
- inputString: string,
- regex: RegExp,
- asyncCallback: (match: RegExpExecArray) => string | Promise
-) {
- let match;
- let previousLastIndex = 0;
- let parts = [];
-
- // While there is a match
- while ((match = regex.exec(inputString)) !== null) {
- // Add the text before the match
- parts.push(inputString.slice(previousLastIndex, match.index));
-
- // Perform the asynchronous operation for the match and add the result
- parts.push(await asyncCallback(match));
-
- // Update the previous last index
- previousLastIndex = regex.lastIndex;
-
- // Avoid infinite loops with zero-width matches
- if (match.index === regex.lastIndex) {
- regex.lastIndex++;
- }
- }
-
- // Add the remaining text
- parts.push(inputString.slice(previousLastIndex));
-
- return parts.join('');
-}