@emulsify/core 3.5.0 → 4.0.1
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.
- package/.cli/init.js +40 -31
- package/.storybook/_drupal.js +129 -8
- package/.storybook/css-components.js +13 -0
- package/.storybook/css-dist.js +5 -0
- package/.storybook/emulsifyTheme.js +9 -6
- package/.storybook/main.js +397 -106
- package/.storybook/manager.js +9 -16
- package/.storybook/preview.js +88 -110
- package/.storybook/utils.js +69 -74
- package/README.md +110 -59
- package/config/.stylelintrc.json +2 -6
- package/config/a11y.config.js +9 -5
- package/config/babel.config.js +6 -11
- package/config/eslint.config.js +31 -3
- package/config/postcss.config.js +5 -0
- package/config/vite/entries.js +227 -0
- package/config/vite/environment.js +39 -0
- package/config/vite/platforms.js +70 -0
- package/config/vite/plugins/copy-src-assets.js +76 -0
- package/config/vite/plugins/copy-twig-files.js +84 -0
- package/config/vite/plugins/css-asset-relativizer.js +40 -0
- package/config/vite/plugins/index.js +105 -0
- package/config/vite/plugins/mirror-components.js +358 -0
- package/config/vite/plugins/require-context.js +311 -0
- package/config/vite/plugins/source-file-index.js +184 -0
- package/config/vite/plugins/svg-sprite.js +117 -0
- package/config/vite/plugins/twig-extension-installers.js +36 -0
- package/config/vite/plugins/twig-module.js +1251 -0
- package/config/vite/plugins/virtual-twig-asset-sources.js +404 -0
- package/config/vite/plugins/virtual-twig-globs.js +136 -0
- package/config/vite/plugins/vituum-patch.js +167 -0
- package/config/vite/plugins/yaml-module.js +133 -0
- package/config/vite/plugins.js +12 -0
- package/config/vite/project-config.js +192 -0
- package/config/vite/project-extensions.js +177 -0
- package/config/vite/project-structure.js +447 -0
- package/config/vite/twig-extensions.js +109 -0
- package/config/vite/utils/fs-safe.js +66 -0
- package/config/vite/utils/paths.js +40 -0
- package/config/vite/utils/react-singleton.js +85 -0
- package/config/vite/utils/unique.js +36 -0
- package/config/vite/vite.config.js +161 -0
- package/package.json +164 -75
- package/scripts/a11y.js +70 -16
- package/scripts/audit-twig-stories.js +378 -0
- package/scripts/audit.js +1602 -0
- package/scripts/check-node-version.js +18 -0
- package/scripts/loadYaml.js +5 -1
- package/src/extensions/index.js +8 -0
- package/src/extensions/react/index.js +12 -0
- package/src/extensions/react/register.js +45 -0
- package/src/extensions/shared/attributes.js +308 -0
- package/src/extensions/shared/html.js +41 -0
- package/src/extensions/shared/lists.js +38 -0
- package/src/extensions/shared/object.js +22 -0
- package/src/extensions/twig/function-map.js +20 -0
- package/src/extensions/twig/functions/add-attributes.js +39 -0
- package/src/extensions/twig/functions/bem.js +166 -0
- package/src/extensions/twig/index.js +13 -0
- package/src/extensions/twig/register.js +95 -0
- package/src/extensions/twig/tag-map.js +16 -0
- package/src/extensions/twig/tags/switch.js +266 -0
- package/src/storybook/index.js +14 -0
- package/src/storybook/main-config.js +132 -0
- package/src/storybook/platform-behaviors.js +60 -0
- package/src/storybook/preview-parameters.js +81 -0
- package/src/storybook/render-twig.js +295 -0
- package/src/storybook/twig/drupal-filters.js +7 -0
- package/src/storybook/twig/include-function.js +109 -0
- package/src/storybook/twig/include.js +28 -0
- package/src/storybook/twig/reference-paths.js +294 -0
- package/src/storybook/twig/resolver.js +318 -0
- package/src/storybook/twig/setup.js +39 -0
- package/src/storybook/twig/source-events.js +5 -0
- package/src/storybook/twig/source-extensions.js +24 -0
- package/src/storybook/twig/source-function.js +239 -0
- package/src/storybook/twig/source.js +39 -0
- package/.all-contributorsrc +0 -45
- package/.editorconfig +0 -5
- package/.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md +0 -18
- package/.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md +0 -11
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
- package/.github/dependabot.yml +0 -6
- package/.github/workflows/addtoprojects.yml +0 -21
- package/.github/workflows/contributors.yml +0 -37
- package/.github/workflows/lint.yml +0 -22
- package/.github/workflows/semantic-release.yml +0 -24
- package/.husky/commit-msg +0 -2
- package/.husky/pre-commit +0 -2
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.storybook/polyfills/twig-include.js +0 -40
- package/.storybook/polyfills/twig-resolver.js +0 -70
- package/.storybook/polyfills/twig-source.js +0 -65
- package/.storybook/webpack.config.js +0 -269
- package/CODE_OF_CONDUCT.md +0 -56
- package/commitlint.config.js +0 -5
- package/config/jest.config.js +0 -19
- package/config/webpack/app.js +0 -1
- package/config/webpack/loaders.js +0 -167
- package/config/webpack/optimizers.js +0 -26
- package/config/webpack/plugins.js +0 -283
- package/config/webpack/resolves.js +0 -157
- package/config/webpack/sdc-loader.js +0 -16
- package/config/webpack/webpack.common.js +0 -272
- package/config/webpack/webpack.dev.js +0 -41
- package/config/webpack/webpack.prod.js +0 -6
- package/release.config.cjs +0 -30
- package/scripts/a11y.test.js +0 -172
- package/scripts/loadYaml.test.js +0 -30
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Platform-specific Storybook behavior helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const genericStorybookAdapter = {
|
|
6
|
+
loadDrupalBehaviorShim: false,
|
|
7
|
+
attachDrupalBehaviors: false,
|
|
8
|
+
registerDrupalTwigFilters: false,
|
|
9
|
+
loadMirroredComponentCss: false,
|
|
10
|
+
allowSyncXhrSource: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize optional platform adapter flags into the full Storybook shape.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} [adapter] - Candidate Storybook adapter flags.
|
|
17
|
+
* @returns {object} Storybook adapter flags with generic defaults.
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeStorybookPlatformAdapter(adapter = {}) {
|
|
20
|
+
return {
|
|
21
|
+
...genericStorybookAdapter,
|
|
22
|
+
...(adapter || {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Attach platform-specific behaviors after a Storybook render.
|
|
28
|
+
*
|
|
29
|
+
* Drupal behavior attachment is opt-in through the active platform adapter.
|
|
30
|
+
* Generic and unknown platforms return without creating Drupal globals.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} [options={}] - Attachment options.
|
|
33
|
+
* @param {object} [options.adapter] - Active Storybook platform adapter.
|
|
34
|
+
* @param {Promise} [options.behaviorShimReady] - Optional behavior shim import.
|
|
35
|
+
* @param {HTMLElement|Document} [options.context] - Behavior attachment root.
|
|
36
|
+
* @param {object} [options.settings] - Behavior settings.
|
|
37
|
+
* @returns {Promise<boolean>} TRUE when Drupal attachBehaviors ran.
|
|
38
|
+
*/
|
|
39
|
+
export async function attachStorybookBehaviors(options = {}) {
|
|
40
|
+
const adapter = normalizeStorybookPlatformAdapter(options.adapter);
|
|
41
|
+
if (!adapter.attachDrupalBehaviors) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await (options.behaviorShimReady || Promise.resolve());
|
|
46
|
+
|
|
47
|
+
const browserWindow = globalThis.window;
|
|
48
|
+
const drupal = browserWindow?.Drupal || globalThis.Drupal;
|
|
49
|
+
if (typeof drupal?.attachBehaviors !== 'function') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
drupal.attachBehaviors(
|
|
54
|
+
options.context,
|
|
55
|
+
options.settings ||
|
|
56
|
+
browserWindow?.drupalSettings ||
|
|
57
|
+
globalThis.drupalSettings,
|
|
58
|
+
);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Storybook preview parameter override helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Determine whether a value is a plain object suitable for recursive merging.
|
|
7
|
+
*
|
|
8
|
+
* @param {*} value - Candidate value.
|
|
9
|
+
* @returns {boolean} TRUE when value is a plain object.
|
|
10
|
+
*/
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return (
|
|
13
|
+
Boolean(value) &&
|
|
14
|
+
Object.prototype.toString.call(value) === '[object Object]'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Merge Storybook preview parameters while preserving nested defaults.
|
|
20
|
+
*
|
|
21
|
+
* Arrays and non-object values are intentionally replaced by overrides. Plain
|
|
22
|
+
* objects merge recursively so partial `a11y.config` overrides keep defaults
|
|
23
|
+
* such as the enabled rule list unless a project explicitly replaces them.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} [defaults={}] - Default Storybook parameters.
|
|
26
|
+
* @param {object} [overrides={}] - Project override parameters.
|
|
27
|
+
* @returns {object} Merged parameters.
|
|
28
|
+
*/
|
|
29
|
+
export function mergePreviewParameters(defaults = {}, overrides = {}) {
|
|
30
|
+
const merged = { ...defaults };
|
|
31
|
+
|
|
32
|
+
for (const [key, value] of Object.entries(overrides || {})) {
|
|
33
|
+
if (value === undefined) continue;
|
|
34
|
+
|
|
35
|
+
// Storybook parameter keys are intentionally dynamic.
|
|
36
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
37
|
+
const current = merged[key];
|
|
38
|
+
const nextValue =
|
|
39
|
+
isPlainObject(current) && isPlainObject(value)
|
|
40
|
+
? mergePreviewParameters(current, value)
|
|
41
|
+
: value;
|
|
42
|
+
|
|
43
|
+
// Storybook parameter keys are intentionally dynamic.
|
|
44
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
45
|
+
merged[key] = nextValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract parameter overrides from a Vite-imported project preview module.
|
|
53
|
+
*
|
|
54
|
+
* Supports both direct parameter objects:
|
|
55
|
+
* export default { layout: 'centered' }
|
|
56
|
+
*
|
|
57
|
+
* And Storybook-shaped modules:
|
|
58
|
+
* export const parameters = { layout: 'centered' }
|
|
59
|
+
* export default { parameters: { layout: 'centered' } }
|
|
60
|
+
*
|
|
61
|
+
* @param {object} [module] - Imported preview override module.
|
|
62
|
+
* @returns {object} Preview parameter overrides.
|
|
63
|
+
*/
|
|
64
|
+
export function normalizePreviewOverrideModule(module = {}) {
|
|
65
|
+
const defaultExport = module?.default;
|
|
66
|
+
|
|
67
|
+
if (isPlainObject(module?.parameters)) {
|
|
68
|
+
return module.parameters;
|
|
69
|
+
}
|
|
70
|
+
if (isPlainObject(defaultExport?.parameters)) {
|
|
71
|
+
return defaultExport.parameters;
|
|
72
|
+
}
|
|
73
|
+
if (isPlainObject(defaultExport)) {
|
|
74
|
+
return defaultExport;
|
|
75
|
+
}
|
|
76
|
+
if (isPlainObject(module)) {
|
|
77
|
+
return module;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file React Storybook renderer for imported Twig template modules.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useReducer, useRef } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
attachStorybookBehaviors,
|
|
8
|
+
normalizeStorybookPlatformAdapter,
|
|
9
|
+
} from './platform-behaviors.js';
|
|
10
|
+
import { TWIG_SOURCE_LOADED_EVENT } from './twig/source-events.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read the normalized Emulsify environment injected by Storybook's Vite config.
|
|
14
|
+
*
|
|
15
|
+
* @returns {object} Injected Emulsify environment, when present.
|
|
16
|
+
*/
|
|
17
|
+
function getInjectedEnvironment() {
|
|
18
|
+
return globalThis.__EMULSIFY_ENV__ || {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize either a full platform adapter or the storybook adapter subset.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} [adapter] - Candidate platform adapter.
|
|
25
|
+
* @returns {object} Storybook adapter flags.
|
|
26
|
+
*/
|
|
27
|
+
export function getActiveStorybookAdapter(options = {}) {
|
|
28
|
+
const adapter =
|
|
29
|
+
options.platformAdapter || getInjectedEnvironment().platformAdapter;
|
|
30
|
+
return normalizeStorybookPlatformAdapter(adapter?.storybook || adapter);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build Twig context from Storybook args and optional static defaults.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} [args={}] - Storybook args.
|
|
37
|
+
* @param {{ context?: object|Function }} [options={}] - Render options.
|
|
38
|
+
* @param {object} [storyContext={}] - Storybook story context.
|
|
39
|
+
* @returns {object} Twig render context.
|
|
40
|
+
*/
|
|
41
|
+
function buildTwigContext(args = {}, options = {}, storyContext = {}) {
|
|
42
|
+
if (typeof options.context === 'function') {
|
|
43
|
+
return options.context(args, storyContext) || {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...(options.context || {}),
|
|
48
|
+
...(args || {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check whether a string looks like rendered HTML markup.
|
|
54
|
+
*
|
|
55
|
+
* This deliberately stays conservative. Plain text story output should remain
|
|
56
|
+
* plain text, while legacy Twig stories that return markup can be promoted to
|
|
57
|
+
* React-managed HTML rendering.
|
|
58
|
+
*
|
|
59
|
+
* @param {*} value - Candidate story result.
|
|
60
|
+
* @returns {boolean} TRUE when the value appears to be an HTML string.
|
|
61
|
+
*/
|
|
62
|
+
function isLikelyHtmlString(value) {
|
|
63
|
+
return (
|
|
64
|
+
typeof value === 'string' &&
|
|
65
|
+
/^\s*<(?:!doctype|!--|[a-z][\w:-]*)(?:\s|>|\/)/i.test(value)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Give Storybook's React element wrapper a useful string representation.
|
|
71
|
+
*
|
|
72
|
+
* Some legacy stories include decorators like `(story) => `${story()}``.
|
|
73
|
+
* Storybook React now passes those decorators a React element, which normally
|
|
74
|
+
* stringifies to `[object Object]`. This preserves the old behavior for Twig
|
|
75
|
+
* string stories while leaving the element renderable by React.
|
|
76
|
+
*
|
|
77
|
+
* @param {*} result - Candidate React story element.
|
|
78
|
+
* @param {Function} getStoryResult - Function returning the raw story result.
|
|
79
|
+
* @returns {*} Original or cloned story result.
|
|
80
|
+
*/
|
|
81
|
+
export function withLegacyStoryToString(result, getStoryResult) {
|
|
82
|
+
if (!React.isValidElement(result) || typeof getStoryResult !== 'function') {
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const legacyCompatibleElement = { ...result };
|
|
87
|
+
Object.defineProperty(legacyCompatibleElement, 'toString', {
|
|
88
|
+
value: () => {
|
|
89
|
+
try {
|
|
90
|
+
const storyResult = getStoryResult();
|
|
91
|
+
return storyResult == null ? '' : String(storyResult);
|
|
92
|
+
} catch {
|
|
93
|
+
return Object.prototype.toString.call(result);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return legacyCompatibleElement;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract legacy Twig HTML from a React story element with a custom toString.
|
|
103
|
+
*
|
|
104
|
+
* `withLegacyStoryToString()` marks legacy Storybook results by installing a
|
|
105
|
+
* custom `toString()`. Reading that value here lets the preview decorator route
|
|
106
|
+
* legacy Twig strings through `TwigHtmlStory` without invoking the story a
|
|
107
|
+
* second time.
|
|
108
|
+
*
|
|
109
|
+
* @param {*} reactElement - Candidate React story element.
|
|
110
|
+
* @returns {string|undefined} Legacy HTML string when detected.
|
|
111
|
+
*/
|
|
112
|
+
export function legacyStringFromElement(reactElement) {
|
|
113
|
+
if (!React.isValidElement(reactElement)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (reactElement.toString === Object.prototype.toString) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const legacyHtml = String(reactElement);
|
|
122
|
+
if (legacyHtml === '[object Object]') {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (typeof legacyHtml !== 'string') {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (!isLikelyHtmlString(legacyHtml)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return legacyHtml;
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Render an imported Twig module into an HTML string.
|
|
139
|
+
*
|
|
140
|
+
* @param {Function} template - Twig render function.
|
|
141
|
+
* @param {object} [args={}] - Storybook args.
|
|
142
|
+
* @param {object} [options={}] - Render options.
|
|
143
|
+
* @param {object} [storyContext={}] - Storybook story context.
|
|
144
|
+
* @returns {string} Rendered HTML.
|
|
145
|
+
*/
|
|
146
|
+
export function renderTwigToHtml(
|
|
147
|
+
template,
|
|
148
|
+
args = {},
|
|
149
|
+
options = {},
|
|
150
|
+
storyContext = {},
|
|
151
|
+
) {
|
|
152
|
+
if (typeof template !== 'function') {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const renderedHtml = template(
|
|
158
|
+
buildTwigContext(args, options, storyContext),
|
|
159
|
+
);
|
|
160
|
+
return renderedHtml == null ? '' : String(renderedHtml);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return `An error occurred whilst rendering Twig story: ${
|
|
163
|
+
error?.message || error
|
|
164
|
+
}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* React component that renders an HTML string into a stable wrapper element.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} props - Component props.
|
|
172
|
+
* @param {string} [props.html] - Rendered HTML string.
|
|
173
|
+
* @param {object} [props.options] - Render options.
|
|
174
|
+
* @returns {React.ReactElement} React element.
|
|
175
|
+
*/
|
|
176
|
+
export function TwigHtmlStory({ html = '', options = {} }) {
|
|
177
|
+
const wrapperRef = useRef(null);
|
|
178
|
+
const adapter = getActiveStorybookAdapter(options);
|
|
179
|
+
const WrapperElement = options.wrapper || 'div';
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
void attachStorybookBehaviors({
|
|
183
|
+
adapter,
|
|
184
|
+
context: wrapperRef.current,
|
|
185
|
+
});
|
|
186
|
+
}, [adapter.attachDrupalBehaviors, html]);
|
|
187
|
+
|
|
188
|
+
return React.createElement(WrapperElement, {
|
|
189
|
+
ref: wrapperRef,
|
|
190
|
+
id: options.id,
|
|
191
|
+
className: options.className,
|
|
192
|
+
'data-emulsify-twig-story': '',
|
|
193
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* React component that renders Twig HTML into a stable wrapper element.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} props - Component props.
|
|
201
|
+
* @param {Function} props.template - Twig render function.
|
|
202
|
+
* @param {object} [props.args] - Storybook args.
|
|
203
|
+
* @param {object} [props.options] - Render options.
|
|
204
|
+
* @param {object} [props.storyContext] - Storybook story context.
|
|
205
|
+
* @returns {React.ReactElement} React element.
|
|
206
|
+
*/
|
|
207
|
+
export function TwigStory({
|
|
208
|
+
template,
|
|
209
|
+
args = {},
|
|
210
|
+
options = {},
|
|
211
|
+
storyContext = {},
|
|
212
|
+
}) {
|
|
213
|
+
const [, rerender] = useReducer((version) => version + 1, 0);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (typeof window === 'undefined') {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Lazy `source()` calls dispatch this event after their dynamic import has
|
|
222
|
+
* populated the synchronous cache. Re-rendering here gives Twig stories the
|
|
223
|
+
* resolved source text on the next pass without blocking the first render.
|
|
224
|
+
*/
|
|
225
|
+
window.addEventListener(TWIG_SOURCE_LOADED_EVENT, rerender);
|
|
226
|
+
return () => {
|
|
227
|
+
window.removeEventListener(TWIG_SOURCE_LOADED_EVENT, rerender);
|
|
228
|
+
};
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
return React.createElement(TwigHtmlStory, {
|
|
232
|
+
html: renderTwigToHtml(template, args, options, storyContext),
|
|
233
|
+
options,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Render a raw HTML string through the same wrapper used by Twig stories.
|
|
239
|
+
*
|
|
240
|
+
* This supports legacy Storybook stories that return Twig HTML strings
|
|
241
|
+
* directly while projects migrate to `renderTwig()`.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} html - Rendered HTML.
|
|
244
|
+
* @param {object} [options={}] - Render options.
|
|
245
|
+
* @returns {React.ReactElement} React element.
|
|
246
|
+
*/
|
|
247
|
+
export function renderTwigHtml(html, options = {}) {
|
|
248
|
+
return React.createElement(TwigHtmlStory, {
|
|
249
|
+
html: html == null ? '' : String(html),
|
|
250
|
+
options,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Convert legacy string-returning Storybook results into React elements.
|
|
256
|
+
*
|
|
257
|
+
* React stories and other non-string results pass through unchanged.
|
|
258
|
+
*
|
|
259
|
+
* @param {*} result - Story render result.
|
|
260
|
+
* @param {object} [options={}] - Render options for string results.
|
|
261
|
+
* @returns {*} React element for strings, otherwise the original result.
|
|
262
|
+
*/
|
|
263
|
+
export function renderHtmlStoryResult(result, options = {}) {
|
|
264
|
+
if (typeof result === 'string') {
|
|
265
|
+
return renderTwigHtml(result, options);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const legacyHtml = legacyStringFromElement(result);
|
|
269
|
+
if (typeof legacyHtml === 'string') {
|
|
270
|
+
return renderTwigHtml(legacyHtml, options);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Create a React-compatible Storybook render function for a Twig template.
|
|
278
|
+
*
|
|
279
|
+
* @param {Function} template - Imported Twig module render function.
|
|
280
|
+
* @param {object} [options={}] - Render options.
|
|
281
|
+
* @returns {Function} Storybook render function.
|
|
282
|
+
*/
|
|
283
|
+
export function renderTwig(template, options = {}) {
|
|
284
|
+
const EmulsifyTwigStoryRender = (args = {}, storyContext = {}) =>
|
|
285
|
+
React.createElement(TwigStory, {
|
|
286
|
+
template,
|
|
287
|
+
args,
|
|
288
|
+
options,
|
|
289
|
+
storyContext,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
EmulsifyTwigStoryRender.displayName = 'EmulsifyTwigStoryRender';
|
|
293
|
+
|
|
294
|
+
return EmulsifyTwigStoryRender;
|
|
295
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Twig include() function factory shared by Storybook Twig renderers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const missingTemplateResolver = () => undefined;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalize optional include arguments into one options object.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} variables - Explicit include variables.
|
|
11
|
+
* @param {boolean|Object} withContext - Twig with-context flag or options.
|
|
12
|
+
* @param {boolean} ignoreMissing - Twig ignore-missing flag.
|
|
13
|
+
* @returns {{variables: Object, withContext: boolean, ignoreMissing: boolean}}
|
|
14
|
+
* Normalized include arguments.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeIncludeOptions(
|
|
17
|
+
variables = {},
|
|
18
|
+
withContext = false,
|
|
19
|
+
ignoreMissing = false,
|
|
20
|
+
) {
|
|
21
|
+
const normalizedVariables =
|
|
22
|
+
variables && typeof variables === 'object' && !Array.isArray(variables)
|
|
23
|
+
? { ...variables }
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
if (typeof normalizedVariables.with_context !== 'undefined') {
|
|
27
|
+
withContext = normalizedVariables.with_context;
|
|
28
|
+
delete normalizedVariables.with_context;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof normalizedVariables.ignore_missing !== 'undefined') {
|
|
32
|
+
ignoreMissing = normalizedVariables.ignore_missing;
|
|
33
|
+
delete normalizedVariables.ignore_missing;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (withContext && typeof withContext === 'object') {
|
|
37
|
+
const optionsObject = withContext;
|
|
38
|
+
|
|
39
|
+
if (typeof optionsObject.with_context !== 'undefined') {
|
|
40
|
+
withContext = optionsObject.with_context;
|
|
41
|
+
}
|
|
42
|
+
if (typeof optionsObject.ignore_missing !== 'undefined') {
|
|
43
|
+
ignoreMissing = optionsObject.ignore_missing;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
variables: normalizedVariables,
|
|
49
|
+
withContext: Boolean(withContext),
|
|
50
|
+
ignoreMissing: Boolean(ignoreMissing),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find the first resolvable include target.
|
|
56
|
+
*
|
|
57
|
+
* @param {string|string[]} templateName - Template name or ordered candidates.
|
|
58
|
+
* @param {Function} resolver - Template resolver.
|
|
59
|
+
* @returns {Function|undefined} Resolved template render function.
|
|
60
|
+
*/
|
|
61
|
+
function resolveIncludeTarget(templateName, resolver) {
|
|
62
|
+
const names = Array.isArray(templateName) ? templateName : [templateName];
|
|
63
|
+
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
const template = resolver(name);
|
|
66
|
+
if (template) {
|
|
67
|
+
return template;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a Twig.js `include()` function for Storybook rendering.
|
|
76
|
+
*
|
|
77
|
+
* @param {Function} resolver - Template resolver.
|
|
78
|
+
* @returns {Function} Twig.js function implementation.
|
|
79
|
+
*/
|
|
80
|
+
export function createTwigIncludeFunction(resolver = missingTemplateResolver) {
|
|
81
|
+
return function include(templateName, variables, withContext, ignoreMissing) {
|
|
82
|
+
const options = normalizeIncludeOptions(
|
|
83
|
+
variables,
|
|
84
|
+
withContext,
|
|
85
|
+
ignoreMissing,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const templateFn = resolveIncludeTarget(templateName, resolver);
|
|
90
|
+
if (!templateFn) {
|
|
91
|
+
if (!options.ignoreMissing) {
|
|
92
|
+
console.error(`Twig include() could not resolve: ${templateName}`);
|
|
93
|
+
}
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const finalContext = options.withContext
|
|
98
|
+
? { ...(this?.context || {}), ...options.variables }
|
|
99
|
+
: options.variables;
|
|
100
|
+
|
|
101
|
+
return templateFn(finalContext);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!options.ignoreMissing) {
|
|
104
|
+
console.error(`Twig include() failed for: ${templateName}`, error);
|
|
105
|
+
}
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Twig include() runtime helper for Storybook-rendered templates.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createTwigIncludeFunction as createIncludeFunction } from './include-function.js';
|
|
6
|
+
import resolveTemplate from './resolver.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a Twig.js `include()` function with the Storybook template resolver.
|
|
10
|
+
*
|
|
11
|
+
* @param {Function} resolver - Template resolver.
|
|
12
|
+
* @returns {Function} Twig.js function implementation.
|
|
13
|
+
*/
|
|
14
|
+
export function createTwigIncludeFunction(resolver = resolveTemplate) {
|
|
15
|
+
return createIncludeFunction(resolver);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Twig `include()` runtime helper.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} Twig - Twig.js module.
|
|
22
|
+
* @returns {undefined}
|
|
23
|
+
*/
|
|
24
|
+
function twigInclude(Twig) {
|
|
25
|
+
Twig.extendFunction('include', createIncludeFunction(resolveTemplate));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default twigInclude;
|