diff --git a/README.md b/README.md
index 26a12a3..642118c 100644
--- a/README.md
+++ b/README.md
@@ -438,3 +438,52 @@ app.get((req, res) => {
}));
})
```
+
+## Alternative usage
+
+With parts renderers `generateRenderContent`, `renderHeadContent`, `renderBodyContent` via html streaming:
+
+```js
+import express from 'express';
+import {createRenderFunction} from '@gravity-ui/app-layout';
+
+const app = express();
+
+const renderLayout = createRenderFunction();
+
+app.get('/', async function (req, res) {
+ res.writeHead(200, {
+ 'Content-Type': 'text/html',
+ 'Transfer-Encoding': 'chunked',
+ });
+
+ const content = generateRenderContent(plugins, {
+ title: 'Home page',
+ });
+
+ const {htmlAttributes, helpers, bodyContent} = content;
+
+ res.write(`
+
+
+
+ ${renderHeadContent(content)}
+
+
+ ${renderBodyContent(content)}
+ `);
+
+ const data = await getUserData();
+
+ res.write(`
+ ${content.renderHelpers.renderInlineScript(`
+ window.__DATA__ = ${htmlescape(data)};
+ `)}
+
+
+ `);
+ res.end();
+});
+
+app.listen(3000);
+```
diff --git a/src/index.ts b/src/index.ts
index c6f42e3..2d2d1f6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,7 @@
-export {createRenderFunction, generateRenderContent} from './render.js';
+export {generateRenderContent} from './utils/generateRenderContent';
+export {renderHeadContent} from './utils/renderHeadContent';
+export {renderBodyContent} from './utils/renderBodyContent';
+export {createRenderFunction} from './render.js';
export {
createGoogleAnalyticsPlugin,
createYandexMetrikaPlugin,
diff --git a/src/render.ts b/src/render.ts
index e6f8e35..3f12f88 100644
--- a/src/render.ts
+++ b/src/render.ts
@@ -1,155 +1,22 @@
-import htmlescape from 'htmlescape';
-
-import type {Icon, Meta, Plugin, RenderContent, RenderParams} from './types.js';
-import {attrs, getRenderHelpers, hasProperty} from './utils.js';
-
-function getRootClassName(theme?: string) {
- if (!theme) {
- return [];
- }
- const classes = ['g-root'];
- if (theme) {
- classes.push(`g-root_theme_${theme}`);
- }
- return classes;
-}
-
-const defaultIcon: Icon = {
- type: 'image/png',
- sizes: '16x16',
- href: '/favicon.png',
-};
-
-const defaultMeta: Meta[] = [
- {
- name: 'viewport',
- content: 'width=device-width, initial-scale=1.0',
- },
-];
-
-/**
- * Generates the content to be rendered in an HTML page.
- * @param plugins - An array of plugins.
- * @param params - An object containing various parameters for rendering the content.
- * @returns An object containing the generated content for an HTML page.
- */
-export function generateRenderContent(
- plugins: Plugins | undefined,
- params: RenderParams,
-): RenderContent {
- const helpers = getRenderHelpers(params);
- const htmlAttributes: Record = {};
- const meta = params.meta ?? [];
- // in terms of sets: meta = params.meta ∪ (defaultMeta ∖ params.meta)
- defaultMeta.forEach((defaultMetaItem) => {
- if (!meta.find(({name}) => name === defaultMetaItem.name)) {
- meta.push(defaultMetaItem);
- }
- });
- const styleSheets = params.styleSheets || [];
- const scripts = params.scripts || [];
- const inlineStyleSheets = params.inlineStyleSheets || [];
- const inlineScripts = params.inlineScripts || [];
- const links = params.links || [];
-
- inlineScripts.unshift(`window.__DATA__ = ${htmlescape(params.data || {})};`);
-
- const content = params.bodyContent ?? {};
- const {theme, className} = content;
- const bodyClasses = Array.from(
- new Set([...getRootClassName(theme), ...(className ? className.split(' ') : [])]),
- );
- const bodyContent = {
- className: bodyClasses,
- root: content.root,
- beforeRoot: content.beforeRoot ? [content.beforeRoot] : [],
- afterRoot: content.afterRoot ? [content.afterRoot] : [],
- };
-
- const {lang, isMobile, title, pluginsOptions = {}} = params;
- for (const plugin of plugins ?? []) {
- plugin.apply({
- options: hasProperty(pluginsOptions, plugin.name)
- ? pluginsOptions[plugin.name]
- : undefined,
- renderContent: {
- htmlAttributes,
- meta,
- links,
- styleSheets,
- scripts,
- inlineStyleSheets,
- inlineScripts,
- bodyContent,
- },
- commonOptions: {title, lang, isMobile},
- utils: helpers,
- });
- }
-
- if (lang) {
- htmlAttributes.lang = lang;
- }
-
- return {
- htmlAttributes,
- meta,
- links,
- styleSheets,
- scripts,
- inlineStyleSheets,
- inlineScripts,
- bodyContent,
- };
-}
+import type {Plugin, RenderParams} from './types.js';
+import {generateRenderContent} from './utils/generateRenderContent';
+import {renderBodyContent} from './utils/renderBodyContent';
+import {renderHeadContent} from './utils/renderHeadContent';
export function createRenderFunction(plugins?: Plugins) {
return function render(params: RenderParams) {
- const {
- htmlAttributes,
- meta,
- styleSheets,
- scripts,
- inlineStyleSheets,
- inlineScripts,
- links,
- bodyContent,
- } = generateRenderContent(plugins, params);
-
- const helpers = getRenderHelpers(params);
+ const content = generateRenderContent(plugins, params);
- const icon: Icon = {
- ...defaultIcon,
- ...params.icon,
- };
+ const {htmlAttributes, helpers, bodyContent} = content;
return `
-
+
-
- ${params.title}
-
- ${[
- ...scripts.map(({src, crossOrigin}) =>
- helpers.renderLink({href: src, crossOrigin, rel: 'preload', as: 'script'}),
- ),
- ...links.map((link) => helpers.renderLink(link)),
- ...meta.map((metaData) => helpers.renderMeta(metaData)),
- ...styleSheets.map((style) => helpers.renderStyle(style)),
- ...inlineStyleSheets.map((text) => helpers.renderInlineStyle(text)),
- ...scripts.map((script) => helpers.renderScript(script)),
- ...inlineScripts.map((text) => helpers.renderInlineScript(text)),
- ]
- .filter(Boolean)
- .join('\n')}
+ ${renderHeadContent(content)}
-
- ${bodyContent.beforeRoot.join('\n')}
-
- ${bodyContent.root ?? ''}
-
- ${bodyContent.afterRoot.join('\n')}
+
+ ${renderBodyContent(content)}
`.trim();
diff --git a/src/types.ts b/src/types.ts
index 69878bd..61d762a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -36,6 +36,14 @@ export interface CommonOptions {
isMobile?: boolean;
}
+export interface BodyContent {
+ attributes: Record;
+ className: string[];
+ beforeRoot: string[];
+ root?: string;
+ afterRoot: string[];
+}
+
export interface RenderContent {
htmlAttributes: Record;
meta: Meta[];
@@ -44,12 +52,10 @@ export interface RenderContent {
styleSheets: Stylesheet[];
inlineScripts: string[];
inlineStyleSheets: string[];
- bodyContent: {
- className: string[];
- beforeRoot: string[];
- root?: string;
- afterRoot: string[];
- };
+ bodyContent: BodyContent;
+ helpers: RenderHelpers;
+ icon: Icon;
+ title: string;
}
export interface RenderHelpers {
@@ -59,6 +65,7 @@ export interface RenderHelpers {
renderInlineStyle(content: string): string;
renderMeta(meta: Meta): string;
renderLink(link: Link): string;
+ attrs(obj: Attributes): string;
}
export interface Plugin {
name: Name;
@@ -71,6 +78,7 @@ export interface Plugin {
}
export interface RenderParams extends CommonOptions {
data?: Data;
+ skipRenderDataScript?: boolean;
icon?: Icon;
nonce?: string;
// content
diff --git a/src/utils.ts b/src/utils.ts
index b37d235..6202a4e 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -56,6 +56,7 @@ export function getRenderHelpers(params: {nonce?: string}): RenderHelpers {
renderInlineStyle,
renderMeta,
renderLink,
+ attrs,
};
}
diff --git a/src/utils/generateRenderContent.ts b/src/utils/generateRenderContent.ts
new file mode 100644
index 0000000..9517501
--- /dev/null
+++ b/src/utils/generateRenderContent.ts
@@ -0,0 +1,120 @@
+import htmlescape from 'htmlescape';
+
+import {BodyContent, Icon, Meta, Plugin, RenderContent, RenderParams} from '../types';
+import {getRenderHelpers, hasProperty} from '../utils';
+
+function getRootClassName(theme?: string) {
+ if (!theme) {
+ return [];
+ }
+ const classes = ['g-root'];
+ if (theme) {
+ classes.push(`g-root_theme_${theme}`);
+ }
+ return classes;
+}
+
+const defaultIcon: Icon = {
+ type: 'image/png',
+ sizes: '16x16',
+ href: '/favicon.png',
+};
+
+const defaultMeta: Meta[] = [
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1.0',
+ },
+];
+
+/**
+ * Generates the content to be rendered in an HTML page.
+ * @param plugins - An array of plugins.
+ * @param params - An object containing various parameters for rendering the content.
+ * @returns An object containing the generated content for an HTML page.
+ */
+export function generateRenderContent(
+ plugins: Plugins | undefined,
+ params: RenderParams,
+): RenderContent {
+ const helpers = getRenderHelpers(params);
+ const htmlAttributes: Record = {};
+ const meta = params.meta ?? [];
+ // in terms of sets: meta = params.meta ∪ (defaultMeta ∖ params.meta)
+ defaultMeta.forEach((defaultMetaItem) => {
+ if (!meta.find(({name}) => name === defaultMetaItem.name)) {
+ meta.push(defaultMetaItem);
+ }
+ });
+ const styleSheets = params.styleSheets || [];
+ const scripts = params.scripts || [];
+ const inlineStyleSheets = params.inlineStyleSheets || [];
+ const inlineScripts = params.inlineScripts || [];
+ const links = params.links || [];
+
+ if (params.skipRenderDataScript) {
+ inlineScripts.unshift(`window.__DATA__ = ${htmlescape(params.data || {})};`);
+ }
+
+ const content = params.bodyContent ?? {};
+ const {theme, className} = content;
+ const bodyClasses = Array.from(
+ new Set([...getRootClassName(theme), ...(className ? className.split(' ') : [])]),
+ );
+ const bodyContent: BodyContent = {
+ attributes: {
+ class: bodyClasses.filter(Boolean).join(' '),
+ },
+ className: bodyClasses,
+ root: content.root,
+ beforeRoot: content.beforeRoot ? [content.beforeRoot] : [],
+ afterRoot: content.afterRoot ? [content.afterRoot] : [],
+ };
+
+ const icon: Icon = {
+ ...defaultIcon,
+ ...params.icon,
+ };
+
+ const {lang, isMobile, title, pluginsOptions = {}} = params;
+ for (const plugin of plugins ?? []) {
+ plugin.apply({
+ options: hasProperty(pluginsOptions, plugin.name)
+ ? pluginsOptions[plugin.name]
+ : undefined,
+ renderContent: {
+ htmlAttributes,
+ meta,
+ links,
+ styleSheets,
+ scripts,
+ inlineStyleSheets,
+ inlineScripts,
+ bodyContent,
+ helpers,
+ icon,
+ title,
+ },
+ commonOptions: {title, lang, isMobile},
+ utils: helpers,
+ });
+ }
+
+ if (lang) {
+ htmlAttributes.lang = lang;
+ }
+
+ return {
+ htmlAttributes,
+ meta,
+ links,
+ styleSheets,
+ scripts,
+ inlineStyleSheets,
+ inlineScripts,
+ bodyContent,
+ helpers,
+ icon,
+ title,
+ };
+}
diff --git a/src/utils/renderBodyContent.ts b/src/utils/renderBodyContent.ts
new file mode 100644
index 0000000..36fa0da
--- /dev/null
+++ b/src/utils/renderBodyContent.ts
@@ -0,0 +1,13 @@
+import {RenderContent} from '../types';
+
+export function renderBodyContent(content: RenderContent): string {
+ const {bodyContent} = content;
+
+ return `
+ ${bodyContent.beforeRoot.join('\n')}
+
+ ${bodyContent.root ?? ''}
+
+ ${bodyContent.afterRoot.join('\n')}
+ `.trim();
+}
diff --git a/src/utils/renderHeadContent.ts b/src/utils/renderHeadContent.ts
new file mode 100644
index 0000000..3b1a5b7
--- /dev/null
+++ b/src/utils/renderHeadContent.ts
@@ -0,0 +1,34 @@
+import {RenderContent} from '../types';
+
+export function renderHeadContent(content: RenderContent): string {
+ const {
+ icon,
+ scripts,
+ helpers,
+ links,
+ meta,
+ styleSheets,
+ inlineStyleSheets,
+ inlineScripts,
+ title,
+ } = content;
+
+ return `
+
+ ${title}
+
+ ${[
+ ...scripts.map(({src, crossOrigin}) =>
+ helpers.renderLink({href: src, crossOrigin, rel: 'preload', as: 'script'}),
+ ),
+ ...links.map((link) => helpers.renderLink(link)),
+ ...meta.map((metaData) => helpers.renderMeta(metaData)),
+ ...styleSheets.map((style) => helpers.renderStyle(style)),
+ ...inlineStyleSheets.map((text) => helpers.renderInlineStyle(text)),
+ ...scripts.map((script) => helpers.renderScript(script)),
+ ...inlineScripts.map((text) => helpers.renderInlineScript(text)),
+ ]
+ .filter(Boolean)
+ .join('\n')}
+ `.trim();
+}