@wix/origami-lint 1.34.31 → 1.34.33
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/dist/rules/index.js +2 -1
- package/dist/rules/site-app/css-layout-properties.js +9 -15
- package/dist/rules/site-app/data-values.js +49 -12
- package/dist/rules/site-app/layout-design-notes.js +277 -0
- package/dist/rules-api/index.js +2 -1
- package/dist/rules-api/rules-api.js +8 -2
- package/dist/rules-api/utils.js +2 -0
- package/dist/shared/css.js +5 -0
- package/dist/shared/utils.js +4 -0
- package/package.json +2 -2
package/dist/rules/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import cssSelectors from "./site-app/css-selectors.js";
|
|
|
8
8
|
import dataValues from "./site-app/data-values.js";
|
|
9
9
|
import globalStyles from "./site-app/global-styles.js";
|
|
10
10
|
import knownTags from "./site-app/known-tags.js";
|
|
11
|
+
import layoutDesignNotes from "./site-app/layout-design-notes.js";
|
|
11
12
|
import noAbsoluteSection from "./site-app/no-absolute-section.js";
|
|
12
13
|
import pageStructure from "./site-app/page-structure.js";
|
|
13
14
|
import slotAttributes from "./site-app/slot-attributes.js";
|
|
@@ -17,5 +18,5 @@ import themeFontKnown from "./site-app/theme-font-known.js";
|
|
|
17
18
|
import themeRoot from "./site-app/theme-root.js";
|
|
18
19
|
import noFreeCode from "./site-app/no-free-code.js";
|
|
19
20
|
// Execution registries combine already-checked rules; only this boundary erases their individual key unions.
|
|
20
|
-
export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags];
|
|
21
|
+
export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags, layoutDesignNotes];
|
|
21
22
|
export { defineRule } from "./site-app/define-rule.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTENT_TARGET, createCssFileReporter,
|
|
1
|
+
import { CONTENT_TARGET, createCssFileReporter, getLayoutLiteral, OVERFLOW_TARGET } from "../../rules-api/index.js";
|
|
2
2
|
import { defineRule } from "./define-rule.js";
|
|
3
3
|
const CHECKS = ['duplicate_property', 'non_layout_property', 'property_outside_target', 'property_in_wrong_target', 'non_container_target_property', 'missing_property_value', 'incompatible_container_property', 'invalid_layout_value', 'missing_container_display', 'missing_grid_column', 'invalid_spx_context', 'unsupported_flex_wrap', 'child_without_container_layout'];
|
|
4
4
|
// ── the tables (propSpecs.ts) ──
|
|
@@ -248,13 +248,10 @@ function checkItemParents(input) {
|
|
|
248
248
|
for (const child of getItems(input))
|
|
249
249
|
checkContainerParent(child, parent, input);
|
|
250
250
|
}
|
|
251
|
-
/** The
|
|
252
|
-
|
|
253
|
-
const held = rulesApi.getAttributeElements(node).filter(({ attribute }) => !LAYER_ATTRIBUTES.includes(getName(attribute))).map(({ element }) => element);
|
|
254
|
-
return [...rulesApi.getChildElements(node), ...held].flatMap((element) => rulesApi.getComponent(element) ?? []);
|
|
255
|
-
}
|
|
251
|
+
/** The components this element lays out (its items, as the import reads structure.jsx). */
|
|
252
|
+
const getItems = ({ node, rulesApi }) => rulesApi.getItemElements(node).flatMap((element) => rulesApi.getComponent(element) ?? []);
|
|
256
253
|
/** Reports each breakpoint at which the child sets item properties in layout.css while its parent has no `display: grid` or `flex` there or by default; not for a `position: fixed` child (its last position at that breakpoint). */
|
|
257
|
-
function checkContainerParent(child, parent, { report }) {
|
|
254
|
+
function checkContainerParent(child, parent, { report, rulesApi }) {
|
|
258
255
|
const byMedia = new Map();
|
|
259
256
|
for (const prop of Object.keys(ITEM_PROPS)) {
|
|
260
257
|
for (const decl of (child.css.layout[prop] ?? []).filter(isOnElement))
|
|
@@ -263,7 +260,7 @@ function checkContainerParent(child, parent, { report }) {
|
|
|
263
260
|
for (const [media, declarations] of byMedia) {
|
|
264
261
|
if (declarations.filter(({ prop }) => prop === 'position').at(-1)?.decl.value.trim() === 'fixed')
|
|
265
262
|
continue; // the last position wins, as the import merges the scope
|
|
266
|
-
if (isContainerAt(parent, media))
|
|
263
|
+
if (isContainerAt(parent, media, rulesApi))
|
|
267
264
|
continue;
|
|
268
265
|
const childName = nameOf(child);
|
|
269
266
|
const parentName = nameOf(parent);
|
|
@@ -272,10 +269,9 @@ function checkContainerParent(child, parent, { report }) {
|
|
|
272
269
|
report({ check: 'child_without_container_layout', loc: child.loc, message: `layout.css:${line}: ${childName} sets item properties (${props}), but its parent ${parentName} has no container layout${media ? ` at @media ${media}` : ''}: give ${parentName} \`display: grid\` or \`display: flex\`, or remove ${childName}'s item properties.` });
|
|
273
270
|
}
|
|
274
271
|
}
|
|
275
|
-
/** The parent's container type at this breakpoint, as the import derives it: its
|
|
276
|
-
function isContainerAt(parent, media) {
|
|
277
|
-
const
|
|
278
|
-
const isContainer = (at) => ['grid', 'flex'].includes(displays.filter((decl) => decl.media === at).at(-1)?.value.trim() ?? '');
|
|
272
|
+
/** The parent's container type at this breakpoint, as the import derives it: its `display` there (on itself or in its target blocks) when it is grid or flex, else the default's. */
|
|
273
|
+
function isContainerAt(parent, media, rulesApi) {
|
|
274
|
+
const isContainer = (at) => ['grid', 'flex'].includes(rulesApi.getLayoutCss(parent, 'display', at)?.value.trim() ?? '');
|
|
279
275
|
return isContainer(media) || (media !== null && isContainer(null));
|
|
280
276
|
}
|
|
281
277
|
/** On the element itself, not in a target block or on an inner part; at any breakpoint. */
|
|
@@ -335,13 +331,11 @@ function mergeScopes(blocks) {
|
|
|
335
331
|
// ── reading values (valueDeserializers.ts) ──
|
|
336
332
|
/** A value reads as the import reads it: a site layout variable's token, `var(--name, <fallback>)`, by its fallback (layoutVariables.ts literalOf); `--spx(12)` as `12spx`. */
|
|
337
333
|
function isReadable(spec, value) {
|
|
338
|
-
const literal =
|
|
334
|
+
const literal = getLayoutLiteral(value);
|
|
339
335
|
if (spec.values)
|
|
340
336
|
return spec.values.includes(literal);
|
|
341
337
|
return spec.reads ? spec.reads(restoreSpx(literal)) : true;
|
|
342
338
|
}
|
|
343
|
-
/** `var(--name, <fallback>)`: a field connected to one of the site's layout variables, written with the field's own value as the fallback. */
|
|
344
|
-
const LAYOUT_VARIABLE_TOKEN = /^var\(--[A-Za-z_-][\w-]*,\s*(.+)\)$/;
|
|
345
339
|
/** The import reads `--spx(12)` as `12spx` before it reads a value (spxTransformer.ts restoreSpxValue). */
|
|
346
340
|
const restoreSpx = (value) => value.replace(/--spx\((-?\d+(?:\.\d+)?)\)/g, '$1spx');
|
|
347
341
|
const SIZE_KEYWORDS = ['auto', 'fit-content', 'max-content', 'min-content', 'unset'];
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// The "data-values" subject of the converter rules (converter 0.80 at c8ed79b): astValue.ts 99, 123, 133; parseJsx.ts 915, 1223,
|
|
2
2
|
// 1227, 1231, 1235, 1255, 1268, 1289, 1697; projectData.ts 214, 225, 281, 435, 443; enrichImage.ts 94, 105.
|
|
3
3
|
// Converter 0.81 (checked at 0.84, 2c0e21c): the tag's import, not a `type` attr, decides if a component takes data attributes; `type` is data.
|
|
4
|
+
// Tracker KIT-richText (converter 0.87): a rich text's type is Builder.RichText.
|
|
5
|
+
// Tracker OUR-003 (origami-transforms mediaValidation): an <Image>'s literal image uri is a Wix media id.
|
|
6
|
+
// Converter 0.87 (68c71e4): `children` is a data key.
|
|
4
7
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
8
|
import { DATA_KEY, EVENT_ATTRIBUTE, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getProperty, isBound, isOneEditAway, isReadByImport, isReservedAttribute, ownerOf, } from "../../rules-api/index.js";
|
|
6
9
|
import { defineRule } from "./define-rule.js";
|
|
7
|
-
const CHECKS = ['attribute_without_braces', 'non_literal_data', 'duplicate_element_prop', 'invalid_element_props_object', 'invalid_element_props_entry', 'invalid_data_object', 'invalid_element_props_key', 'unsupported_element_props_key', 'mixed_data_wrapper', 'duplicate_data_key', 'bare_document_href', 'missing_document_id', 'inconsistent_image_url', 'misplaced_image_url_call'];
|
|
10
|
+
const CHECKS = ['attribute_without_braces', 'non_literal_data', 'duplicate_element_prop', 'invalid_element_props_object', 'invalid_element_props_entry', 'invalid_data_object', 'invalid_element_props_key', 'unsupported_element_props_key', 'mixed_data_wrapper', 'duplicate_data_key', 'bare_document_href', 'missing_document_id', 'inconsistent_image_url', 'misplaced_image_url_call', 'invalid_rich_text_type', 'invalid_image_media_id'];
|
|
8
11
|
/** The names the import never reads as data (dataKeys.ts). The React name is joined as the converter does: Wix Standards fails the build on it in any file. */
|
|
9
12
|
const EXCLUDED_DATA_KEYS = [
|
|
10
13
|
'_slotsId', '_mapId', 'hidden', 'class', 'title', 'role', 'tabindex', 'draggable', 'lang', 'dir', 'accesskey', 'contenteditable', 'spellcheck',
|
|
11
14
|
'translate', 'autofocus', 'inputmode', 'enterkeyhint', 'popover', 'inert', 'part', 'nonce', 'is', 'itemscope', 'itemtype', 'itemprop', 'itemid',
|
|
12
|
-
'itemref', 'tabIndex', 'contentEditable', 'accessKey', 'autoFocus', 'inputMode', 'enterKeyHint', 'spellCheck', '
|
|
15
|
+
'itemref', 'tabIndex', 'contentEditable', 'accessKey', 'autoFocus', 'inputMode', 'enterKeyHint', 'spellCheck', 'styleRef', 'key',
|
|
13
16
|
'ref', ['dangerouslySet', 'InnerHTML'].join(''), '__proto__', 'constructor', 'prototype',
|
|
14
17
|
];
|
|
15
18
|
/** An elementProps entry's name is a CSS identifier (parseJsx.ts 1231): it becomes a class. */
|
|
@@ -18,9 +21,14 @@ const CSS_IDENTIFIER = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/;
|
|
|
18
21
|
const RESERVED_OBJECT_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
19
22
|
/** An image object's `type` (enrichImage.ts). */
|
|
20
23
|
const IMAGE_TYPE = 'Builder.Image';
|
|
24
|
+
/** A rich text's `type`, and the key the export writes it under (projectData.ts rebuildRichText reads `html` only on this type). */
|
|
25
|
+
const RICH_TEXT_TYPE = 'Builder.RichText';
|
|
26
|
+
const RICH_TEXT_KEY = 'richText';
|
|
21
27
|
/** The media identifiers the export writes: an image's (`wix:image://v1/<uri>/<name>#originWidth=…`), a document's (`wix:document://v1/ugd/<file>/<name>`). */
|
|
22
28
|
const IMAGE_PREFIX = 'wix:image://v1/';
|
|
23
29
|
const DOCUMENT_PREFIX = 'wix:document://v1/';
|
|
30
|
+
/** A Wix media id, `117a96_1cff23187eeb4050a2a691bcdcad6502~mv2.jpg`: an account prefix, an underscore and a file name (mediaValidation/index.js). */
|
|
31
|
+
const WIX_MEDIA_ID = /^[A-Za-z0-9]+_[A-Za-z0-9_~.-]+\.[A-Za-z0-9]+$/;
|
|
24
32
|
/** The reserved attributes this rule reads as data. The strings are component-attributes', `className` and `bindings` the bindings rule's. */
|
|
25
33
|
const READ_AS_DATA = ['data', 'props', 'elementProps'];
|
|
26
34
|
const FULL = { menus: true, media: true, jsx: true };
|
|
@@ -41,8 +49,9 @@ export default defineRule({
|
|
|
41
49
|
},
|
|
42
50
|
});
|
|
43
51
|
// ── the callback's steps ──
|
|
44
|
-
/**
|
|
52
|
+
/** Any <Image> in the file: its media id, as the build checks it. Then a component written out under Structure: its data values, then what the import builds from the ones it read. */
|
|
45
53
|
function checkJSXElementDataValues(input) {
|
|
54
|
+
checkImageMediaId(input);
|
|
46
55
|
if (!input.rulesApi.getComponent(input.node) || !isReadByImport(input.node))
|
|
47
56
|
return;
|
|
48
57
|
const elementProps = getAttribute(input.node, 'elementProps');
|
|
@@ -54,6 +63,7 @@ function checkJSXElementDataValues(input) {
|
|
|
54
63
|
checkDataKeyOnce(input, read.has('data') ? checkDataCarrier(input) : null);
|
|
55
64
|
checkDocumentLinks(input, read);
|
|
56
65
|
checkImageUrls(input, read);
|
|
66
|
+
checkRichTextTypes(input, read);
|
|
57
67
|
}
|
|
58
68
|
// ── the steps ──
|
|
59
69
|
const say = ({ report }, node, check, message) => report({ check, loc: node.loc, message });
|
|
@@ -253,8 +263,8 @@ function hasDocumentId(identifier) {
|
|
|
253
263
|
function checkImageUrls(input, read) {
|
|
254
264
|
const urls = new Set();
|
|
255
265
|
const values = dataValues(input).filter(([, path]) => read.has(attributeOf(path)));
|
|
256
|
-
for (const [value, path] of values) {
|
|
257
|
-
for (const { node, path: at } of objectsIn(value, path)) {
|
|
266
|
+
for (const [value, path, key] of values) {
|
|
267
|
+
for (const { node, path: at } of objectsIn(value, path, key)) {
|
|
258
268
|
if (valueOf(getProperty(node, 'type')?.value ?? null) !== IMAGE_TYPE)
|
|
259
269
|
continue;
|
|
260
270
|
const url = getProperty(node, 'url');
|
|
@@ -274,6 +284,32 @@ function checkImageUrls(input, read) {
|
|
|
274
284
|
if (stray)
|
|
275
285
|
say(input, stray, 'misplaced_image_url_call', `${owner(input)} has \`media.getCroppedImageUrl(…)\` outside an image's url. Write it only as the \`url\` of a \`{ type: 'Builder.Image', … }\` object.`);
|
|
276
286
|
}
|
|
287
|
+
/** KIT-richText: with another `type`, or none, the import takes the rich text but keeps its text empty (live import, 2026-10-26). */
|
|
288
|
+
function checkRichTextTypes(input, read) {
|
|
289
|
+
const values = dataValues(input).filter(([, path]) => read.has(attributeOf(path)));
|
|
290
|
+
for (const [value, path, valueKey] of values) {
|
|
291
|
+
for (const { node, path: at, key } of objectsIn(value, path, valueKey)) {
|
|
292
|
+
if (key !== RICH_TEXT_KEY)
|
|
293
|
+
continue;
|
|
294
|
+
const type = getProperty(node, 'type');
|
|
295
|
+
if (type && valueOf(type.value) === RICH_TEXT_TYPE)
|
|
296
|
+
continue;
|
|
297
|
+
const written = type ? `has \`type: ${textOf(input)(type.value)}\`` : 'has no `type`';
|
|
298
|
+
say(input, type ?? node, 'invalid_rich_text_type', `${owner(input)}'s \`${at}\` ${written}. Write \`type: '${RICH_TEXT_TYPE}'\`: with another type, or none, the import keeps its text empty (tracker KIT-richText).`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/** OUR-003: `yarn build` fails on an <Image> (by tag name, anywhere in the file) whose literal `image.uri` isn't a Wix media id; the import takes it, but the image is broken. */
|
|
303
|
+
function checkImageMediaId(input) {
|
|
304
|
+
const image = getObject(getExpression(getAttribute(input.node, 'image')));
|
|
305
|
+
if (input.rulesApi.getTagName(input.node) !== 'Image' || !image)
|
|
306
|
+
return;
|
|
307
|
+
const uri = getProperty(image, 'uri');
|
|
308
|
+
const value = uri && bare(uri.value);
|
|
309
|
+
if (value?.type !== 'Literal' || typeof value.value !== 'string' || WIX_MEDIA_ID.test(value.value))
|
|
310
|
+
return;
|
|
311
|
+
say(input, value, 'invalid_image_media_id', `${owner(input)}'s \`image.uri\` '${value.value}' isn't a Wix media id, and \`yarn build\` fails on it. Run \`wix origami upload-media\` to upload it (tracker OUR-003).`);
|
|
312
|
+
}
|
|
277
313
|
function imageCallsIn(input, dataValuesRead, read) {
|
|
278
314
|
const values = dataValuesRead.map(([value]) => value);
|
|
279
315
|
const props = read.has('props') ? getExpression(getAttribute(input.node, 'props')) : null;
|
|
@@ -465,29 +501,30 @@ function derivedImageArguments(image) {
|
|
|
465
501
|
];
|
|
466
502
|
}
|
|
467
503
|
const sameArguments = (call, expected) => isDeepStrictEqual(call.arguments.map(valueOf), expected);
|
|
468
|
-
function objectsIn(node, path) {
|
|
504
|
+
function objectsIn(node, path, key) {
|
|
469
505
|
if (node === null)
|
|
470
506
|
return [];
|
|
471
507
|
const value = bare(node);
|
|
472
508
|
if (value.type === 'ArrayExpression')
|
|
473
|
-
return value.elements.flatMap((element, index) => (element && element.type !== 'SpreadElement' ? objectsIn(element, `${path}[${index}]
|
|
509
|
+
return value.elements.flatMap((element, index) => (element && element.type !== 'SpreadElement' ? objectsIn(element, `${path}[${index}]`, null) : []));
|
|
474
510
|
if (value.type !== 'ObjectExpression')
|
|
475
511
|
return [];
|
|
476
|
-
return [{ node: value, path }, ...getProperties(value).flatMap((property) => objectsIn(property.value, `${path}.${getKey(property)}
|
|
512
|
+
return [{ node: value, path, key }, ...getProperties(value).flatMap((property) => objectsIn(property.value, `${path}.${getKey(property)}`, getKey(property)))];
|
|
477
513
|
}
|
|
514
|
+
/** Each data value: the value, its path as a message writes it, and the key that holds it. */
|
|
478
515
|
function dataValues(input) {
|
|
479
516
|
const element = input.node;
|
|
480
|
-
const values = dataAttributes(input).map((attribute) => [getExpression(attribute), getName(attribute)]).filter(([value]) => value);
|
|
517
|
+
const values = dataAttributes(input).map((attribute) => [getExpression(attribute), getName(attribute), getName(attribute)]).filter(([value]) => value);
|
|
481
518
|
const data = getExpression(getAttribute(element, 'data'));
|
|
482
519
|
if (data)
|
|
483
|
-
values.push([data, 'data']);
|
|
520
|
+
values.push([data, 'data', 'data']);
|
|
484
521
|
for (const { entry, path } of getEntries(getObject(getExpression(getAttribute(element, 'elementProps'))))) {
|
|
485
522
|
const at = `elementProps.${path.join('.')}`;
|
|
486
523
|
for (const property of entryDataKeys(entry))
|
|
487
|
-
values.push([property.value, `${at}.${getKey(property)}
|
|
524
|
+
values.push([property.value, `${at}.${getKey(property)}`, getKey(property)]);
|
|
488
525
|
const entryData = getProperty(entry, 'data');
|
|
489
526
|
if (entryData)
|
|
490
|
-
values.push([entryData.value, `${at}.data
|
|
527
|
+
values.push([entryData.value, `${at}.data`, 'data']);
|
|
491
528
|
}
|
|
492
529
|
return values;
|
|
493
530
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// The layout-design-notes subject (tracker tab css-subjects, rows KIT-*): layout choices the editor's later editing depends on, as the kit's
|
|
2
|
+
// notes check them (origami PR #45 at 01510ad, validate.mjs: stackPolicyNotes :250-283, oneColumnGrids :197-223, boxIssues :157-169, noMobile
|
|
3
|
+
// :446, :556). Warnings, never errors: the import accepts every case; the design call stays the author's. In a page's or global component's
|
|
4
|
+
// layout.css, read with its structure.jsx:
|
|
5
|
+
// - a Section, HeaderSection or FooterSection and its canvas (its InternalContainer) are a grid, never `display: flex` (KIT-sectionStack);
|
|
6
|
+
// - a BoxContainer carries no `display: flex`, the stack goes on its InternalContainer (KIT-boxStack), which has no width or height of its
|
|
7
|
+
// own (KIT-icSize); the box of a row stack is `width: max-content`, not a % (KIT-rowStackBox);
|
|
8
|
+
// - an InternalContainer that isn't a canvas and isn't inside a Menu: a one-column grid with two or more children in separate rows is a
|
|
9
|
+
// Stack (KIT-oneColumnGrid, by default only); a stack with one child is a grid (KIT-stackSingle); a stack's children each have their own
|
|
10
|
+
// order, 1 to n (KIT-stackOrder); by default, the orders follow the JSX order (KIT-stackOrderJsx);
|
|
11
|
+
// - when the site or the page has a breakpoint, a component with no rule for it reuses its desktop layout (KIT-noMobile): one line per
|
|
12
|
+
// page and breakpoint, from the page's root.
|
|
13
|
+
// A breakpoint is its `@media` query as written; a breakpoint without a value takes the default's, as the import merges the element's
|
|
14
|
+
// scope (rulesApi.getLayoutCss). A breakpoint's note that repeats the default's (inherited, not restated) is reported once.
|
|
15
|
+
import { FILE_START } from "../../page-model/index.js";
|
|
16
|
+
import { getLayoutLiteral, GLOBAL_SECTIONS, isElementDeclaration, SECTIONS, WRAPPERS } from "../../rules-api/index.js";
|
|
17
|
+
import { defineRule } from "./define-rule.js";
|
|
18
|
+
const CHECKS = ['section_stack', 'box_stack', 'inner_container_size', 'row_stack_box_width', 'one_column_grid', 'stack_single_child', 'stack_order', 'stack_order_jsx', 'no_mobile_rule'];
|
|
19
|
+
export default defineRule({
|
|
20
|
+
name: 'layout-design-notes',
|
|
21
|
+
editors: ['studio2'],
|
|
22
|
+
severity: 'warn',
|
|
23
|
+
checks: CHECKS,
|
|
24
|
+
callbacks: {
|
|
25
|
+
JSXElement: checkElementLayout,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
/** The notes on one element, by what it is: a section, a BoxContainer, an InternalContainer; the page's mobile rules from its root. */
|
|
29
|
+
function checkElementLayout(input) {
|
|
30
|
+
const { node, rulesApi } = input;
|
|
31
|
+
const component = rulesApi.getComponent(node);
|
|
32
|
+
if (!component)
|
|
33
|
+
return;
|
|
34
|
+
const type = rulesApi.getType(node);
|
|
35
|
+
if (type !== null && SECTIONS.includes(type))
|
|
36
|
+
checkSectionIsGrid(component, 'a section', input);
|
|
37
|
+
if (type === 'box')
|
|
38
|
+
checkBox(component, input);
|
|
39
|
+
if (type === 'innerElement')
|
|
40
|
+
checkInternalContainer(component, input);
|
|
41
|
+
if (rulesApi.getRoot() === component)
|
|
42
|
+
checkMobileRules(input);
|
|
43
|
+
}
|
|
44
|
+
// ── a section and its canvas (KIT-sectionStack) ──
|
|
45
|
+
/** A section, or a section's canvas, with `display: flex`. */
|
|
46
|
+
function checkSectionIsGrid(component, what, input) {
|
|
47
|
+
reportAtEachBreakpoint([component], (media) => {
|
|
48
|
+
const display = getLayoutLine(input.rulesApi, component, 'display', media);
|
|
49
|
+
if (!isStack(display))
|
|
50
|
+
return null;
|
|
51
|
+
return { loc: component.loc, line: display.line, text: `${nameOf(component)}, ${what}, has \`display: flex\`: ${what} is always a grid, write \`display: grid\``, row: 'KIT-sectionStack', check: 'section_stack' };
|
|
52
|
+
}, input);
|
|
53
|
+
}
|
|
54
|
+
// ── a BoxContainer and its InternalContainer (KIT-boxStack, KIT-icSize, KIT-rowStackBox) ──
|
|
55
|
+
function checkBox(box, input) {
|
|
56
|
+
const { node, rulesApi } = input;
|
|
57
|
+
const innerElement = rulesApi.getItemElements(node).find((item) => rulesApi.getType(item) === 'innerElement'); // a child, or `children={<InternalContainer …>}`
|
|
58
|
+
const inner = innerElement ? rulesApi.getComponent(innerElement) : null;
|
|
59
|
+
checkBoxStack(box, inner, input);
|
|
60
|
+
if (!inner)
|
|
61
|
+
return;
|
|
62
|
+
checkBoxSize(box, inner, input);
|
|
63
|
+
checkRowStackBoxWidth(box, inner, input);
|
|
64
|
+
}
|
|
65
|
+
/** `display: flex` on the BoxContainer itself. */
|
|
66
|
+
function checkBoxStack(box, inner, input) {
|
|
67
|
+
reportAtEachBreakpoint([box], (media) => {
|
|
68
|
+
const display = getLayoutLine(input.rulesApi, box, 'display', media);
|
|
69
|
+
if (!isStack(display))
|
|
70
|
+
return null;
|
|
71
|
+
return { loc: box.loc, line: display.line, text: `${nameOf(box)} (BoxContainer) has \`display: flex\`: put the stack on its InternalContainer${inner ? `, ${nameOf(inner)}` : ''}`, row: 'KIT-boxStack', check: 'box_stack' };
|
|
72
|
+
}, input);
|
|
73
|
+
}
|
|
74
|
+
/** A width or height of its own on the BoxContainer's InternalContainer: not `100%` or `auto`, which fill the box (the export writes `width: 100%; height: auto`). */
|
|
75
|
+
function checkBoxSize(box, inner, input) {
|
|
76
|
+
for (const property of ['width', 'height']) {
|
|
77
|
+
reportAtEachBreakpoint([inner], (media) => {
|
|
78
|
+
const size = getLayoutLine(input.rulesApi, inner, property, media);
|
|
79
|
+
if (!size || FILLS_BOX.includes(size.value))
|
|
80
|
+
return null;
|
|
81
|
+
return { loc: inner.loc, line: size.line, text: `${nameOf(inner)} has \`${property}: ${size.value}\`: put the size on its BoxContainer, ${nameOf(box)}`, row: 'KIT-icSize', check: 'inner_container_size' };
|
|
82
|
+
}, input);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** An InternalContainer size that fills its BoxContainer rather than sizing it. */
|
|
86
|
+
const FILLS_BOX = ['100%', 'auto'];
|
|
87
|
+
/** A % width on the BoxContainer of a row stack. */
|
|
88
|
+
function checkRowStackBoxWidth(box, inner, input) {
|
|
89
|
+
reportAtEachBreakpoint([box, inner], (media) => {
|
|
90
|
+
const { rulesApi } = input;
|
|
91
|
+
const width = getLayoutLine(rulesApi, box, 'width', media);
|
|
92
|
+
const direction = getLayoutLine(rulesApi, inner, 'flex-direction', media)?.value ?? 'row'; // CSS's default
|
|
93
|
+
if (!width || !width.value.endsWith('%') || !isStack(getLayoutLine(rulesApi, inner, 'display', media)) || !/^row(-reverse)?$/.test(direction))
|
|
94
|
+
return null;
|
|
95
|
+
return { loc: box.loc, line: width.line, text: `${nameOf(box)} has \`width: ${width.value}\`, but its InternalContainer ${nameOf(inner)} is a row stack: a row stack's box follows its content, write \`width: max-content\``, row: 'KIT-rowStackBox', check: 'row_stack_box_width' };
|
|
96
|
+
}, input);
|
|
97
|
+
}
|
|
98
|
+
// ── an InternalContainer's children (KIT-oneColumnGrid, KIT-stackSingle, KIT-stackOrder, KIT-stackOrderJsx) ──
|
|
99
|
+
function checkInternalContainer(inner, input) {
|
|
100
|
+
const holders = getHolders(input.node, input.rulesApi);
|
|
101
|
+
const holderType = holders[0] ? input.rulesApi.getType(holders[0]) : null;
|
|
102
|
+
if (holderType !== null && SECTIONS.includes(holderType))
|
|
103
|
+
return checkSectionIsGrid(inner, "a section's canvas", input);
|
|
104
|
+
if (holders.some((holder) => input.rulesApi.getType(holder) === 'menu'))
|
|
105
|
+
return;
|
|
106
|
+
const items = input.rulesApi.getItemElements(input.node).flatMap((element) => input.rulesApi.getComponent(element) ?? []);
|
|
107
|
+
checkOneColumnGrid(inner, items, input);
|
|
108
|
+
checkStackSingleChild(inner, items, input);
|
|
109
|
+
checkStackOrder(inner, items, input);
|
|
110
|
+
checkStackOrderJsx(inner, items, input);
|
|
111
|
+
}
|
|
112
|
+
/** By default: a one-column grid whose two or more children sit in separate rows. */
|
|
113
|
+
function checkOneColumnGrid(inner, items, { rulesApi, report }) {
|
|
114
|
+
const display = getLayoutLine(rulesApi, inner, 'display', null);
|
|
115
|
+
if (display?.value !== 'grid' || items.length < 2)
|
|
116
|
+
return;
|
|
117
|
+
const columns = (getLayoutLine(rulesApi, inner, 'grid-template-columns', null)?.value ?? '').replace(/^repeat\(\s*1\s*,\s*(.*)\)$/, '$1'); // repeat(1, <tracks>) is its tracks
|
|
118
|
+
if (columns.includes('repeat(') || columns.split(/\s+(?![^(]*\))/).length > 1)
|
|
119
|
+
return;
|
|
120
|
+
if (items.some((item) => !isFirstColumn(getLayoutLine(rulesApi, item, 'grid-column', null))))
|
|
121
|
+
return; // a child past the one track adds a column
|
|
122
|
+
const rows = items.map((item) => getRowRange(getLayoutLine(rulesApi, item, 'grid-row', null))).filter((range) => range !== null);
|
|
123
|
+
const layered = rows.some((a, index) => rows.slice(index + 1).some((b) => a[0] < b[1] && b[0] < a[1]));
|
|
124
|
+
if (layered)
|
|
125
|
+
return;
|
|
126
|
+
reportNote({ loc: inner.loc, line: display.line, text: `${nameOf(inner)} is a one-column grid with ${items.length} children in separate rows: a Stack (\`display: flex; flex-direction: column\`) keeps them editable as a group`, row: 'KIT-oneColumnGrid', check: 'one_column_grid' }, null, report);
|
|
127
|
+
}
|
|
128
|
+
/** Whether a `grid-column` keeps the child in the first column: none, `auto`, `1`, `span 1`, `1 / 2`, `1 / -1` or `1 / span 1`. */
|
|
129
|
+
const isFirstColumn = (gridColumn) => !gridColumn || /^(auto|span\s+1|1(\s*\/\s*(2|-1|span\s+1))?)$/.test(gridColumn.value);
|
|
130
|
+
/** A `grid-row` as the rows it occupies, [start, end): `2 / 4` → [2, 4], `2` → [2, 3]; a line counted from the end (`1 / -1`) as every row;
|
|
131
|
+
* null for anything else (a free row). */
|
|
132
|
+
function getRowRange(gridRow) {
|
|
133
|
+
const match = /^(-?\d+)(?:\s*\/\s*(-?\d+))?$/.exec(gridRow?.value ?? '');
|
|
134
|
+
if (!match)
|
|
135
|
+
return null;
|
|
136
|
+
// Without the row count, a line from the end can't be placed: taken as every row, so the grid isn't noted (read grid-template-rows to place it).
|
|
137
|
+
if (match[1].startsWith('-') || match[2]?.startsWith('-'))
|
|
138
|
+
return [-Infinity, Infinity];
|
|
139
|
+
const start = Number(match[1]);
|
|
140
|
+
const end = match[2] === undefined ? start + 1 : Number(match[2]);
|
|
141
|
+
return end > start ? [start, end] : null;
|
|
142
|
+
}
|
|
143
|
+
/** A stack with one child. */
|
|
144
|
+
function checkStackSingleChild(inner, items, input) {
|
|
145
|
+
if (items.length !== 1)
|
|
146
|
+
return;
|
|
147
|
+
reportAtEachBreakpoint([inner], (media) => {
|
|
148
|
+
const display = getLayoutLine(input.rulesApi, inner, 'display', media);
|
|
149
|
+
if (!isStack(display))
|
|
150
|
+
return null;
|
|
151
|
+
return { loc: inner.loc, line: display.line, text: `${nameOf(inner)} is a stack with one child, ${nameOf(items[0])}: a single child sits in a grid, write \`display: grid\``, row: 'KIT-stackSingle', check: 'stack_single_child' };
|
|
152
|
+
}, input);
|
|
153
|
+
}
|
|
154
|
+
/** A stack's children that share an order, or whose orders (all written) aren't 1 to n. A missing order isn't noted: the import fills the JSX order. */
|
|
155
|
+
function checkStackOrder(inner, items, input) {
|
|
156
|
+
if (items.length < 2)
|
|
157
|
+
return;
|
|
158
|
+
reportAtEachBreakpoint([inner, ...items], (media) => {
|
|
159
|
+
const { rulesApi } = input;
|
|
160
|
+
if (!isStack(getLayoutLine(rulesApi, inner, 'display', media)))
|
|
161
|
+
return null;
|
|
162
|
+
const orders = items.flatMap((item) => { const order = getLayoutLine(rulesApi, item, 'order', media); return order ? [{ item, order }] : []; });
|
|
163
|
+
const byValue = new Map();
|
|
164
|
+
for (const { item, order } of orders)
|
|
165
|
+
byValue.set(order.value, [...(byValue.get(order.value) ?? []), item]);
|
|
166
|
+
const shared = [...byValue].find(([, sharing]) => sharing.length > 1);
|
|
167
|
+
if (shared)
|
|
168
|
+
return { loc: inner.loc, line: orders[0].order.line, text: `stack children ${shared[1].map(nameOf).join(' and ')} share \`order: ${shared[0]}\`: give each child its own order, 1, 2, 3 …`, row: 'KIT-stackOrder', check: 'stack_order' };
|
|
169
|
+
if (orders.length !== items.length || isOneToN(orders.map(({ order }) => Number(order.value))))
|
|
170
|
+
return null;
|
|
171
|
+
return { loc: inner.loc, line: orders[0].order.line, text: `${nameOf(inner)}'s stack children use order ${orders.map(({ order }) => order.value).join(', ')}, not 1 to ${items.length}: give each child its own order, 1, 2, 3 …`, row: 'KIT-stackOrder', check: 'stack_order' };
|
|
172
|
+
}, input);
|
|
173
|
+
}
|
|
174
|
+
/** By default, on a stack that isn't reversed: orders (a clean 1 to n) that show the children in another order than the JSX. */
|
|
175
|
+
function checkStackOrderJsx(inner, items, { rulesApi, report }) {
|
|
176
|
+
if (items.length < 2 || !isStack(getLayoutLine(rulesApi, inner, 'display', null)))
|
|
177
|
+
return;
|
|
178
|
+
if (/reverse/.test(getLayoutLine(rulesApi, inner, 'flex-direction', null)?.value ?? ''))
|
|
179
|
+
return;
|
|
180
|
+
const orders = items.map((item) => getLayoutLine(rulesApi, item, 'order', null));
|
|
181
|
+
if (!orders.every((order) => order !== undefined) || !isOneToN(orders.map((order) => Number(order.value))))
|
|
182
|
+
return;
|
|
183
|
+
const shown = items.map((item, index) => ({ item, order: Number(orders[index].value) })).sort((a, b) => a.order - b.order).map(({ item }) => item);
|
|
184
|
+
if (shown.every((item, index) => item === items[index]))
|
|
185
|
+
return;
|
|
186
|
+
reportNote({ loc: inner.loc, line: orders[0].line, text: `${nameOf(inner)} shows its children as ${shown.map(nameOf).join(', ')}, unlike the JSX order ${items.map(nameOf).join(', ')} (fine when intended)`, row: 'KIT-stackOrderJsx', check: 'stack_order_jsx' }, null, report);
|
|
187
|
+
}
|
|
188
|
+
/** Whether these are 1, 2 … n in some order. */
|
|
189
|
+
const isOneToN = (numbers) => [...numbers].sort((a, b) => a - b).every((number, index) => number === index + 1);
|
|
190
|
+
// ── the page's mobile rules (KIT-noMobile) ──
|
|
191
|
+
/** At each breakpoint the site or the page has: the elements with a default layout rule but no rule for it; one line per breakpoint. Not the
|
|
192
|
+
* root, a reference to a global component (its page lays it out), a layer or a Background (it spans its owner at every breakpoint), or anything inside a Menu (the editor's own layout). */
|
|
193
|
+
function checkMobileRules({ node, rulesApi, report }) {
|
|
194
|
+
const laidOut = getLaidOutComponents(node, rulesApi);
|
|
195
|
+
const siteBreakpoints = rulesApi.getCustomMedia('theme').filter(({ query }) => query);
|
|
196
|
+
/** A breakpoint by its range: `(--tablet)` (or `( --tablet )`) as the theme declares it, `(max-width: 1023px)`; any other as written. */
|
|
197
|
+
const toRange = (media) => {
|
|
198
|
+
const name = media === null ? null : rulesApi.getBreakpointName(media);
|
|
199
|
+
return siteBreakpoints.find((breakpoint) => breakpoint.name === name)?.query ?? media;
|
|
200
|
+
};
|
|
201
|
+
// The page's breakpoints: from every component, the ones not counted too (a Background's `@media` is the page's).
|
|
202
|
+
const breakpoints = new Set([...siteBreakpoints.map(({ query }) => query), ...getBreakpoints(Object.values(rulesApi.page.components)).map(toRange)]);
|
|
203
|
+
for (const breakpoint of breakpoints) {
|
|
204
|
+
const missing = laidOut.filter((component) => hasLayoutAt(component, null) && !hasLayoutAt(component, breakpoint, toRange));
|
|
205
|
+
if (!missing.length)
|
|
206
|
+
continue;
|
|
207
|
+
const names = missing.slice(0, 6).map(nameOf).join(', ') + (missing.length > 6 ? ', …' : '');
|
|
208
|
+
report({ check: 'no_mobile_rule', loc: FILE_START, message: `layout.css: ${missing.length} element${missing.length === 1 ? ' has' : 's have'} no rule for @media ${breakpoint} and reuse${missing.length === 1 ? 's' : ''} the desktop layout as is (fine when intended): ${names} (tracker KIT-noMobile).` });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** The components the page lays out below its root: through every element and its items; not into a Menu, not a global component's reference or a Background. */
|
|
212
|
+
function getLaidOutComponents(root, rulesApi) {
|
|
213
|
+
const found = [];
|
|
214
|
+
const walk = (element) => {
|
|
215
|
+
for (const item of rulesApi.getItemElements(element)) {
|
|
216
|
+
const type = rulesApi.getType(item);
|
|
217
|
+
if (type !== null && (GLOBAL_SECTIONS.includes(type) || type === 'background'))
|
|
218
|
+
continue;
|
|
219
|
+
const component = rulesApi.getComponent(item);
|
|
220
|
+
if (component)
|
|
221
|
+
found.push(component);
|
|
222
|
+
if (type !== 'menu')
|
|
223
|
+
walk(item);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk(root);
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
/** Whether the element's own layout lines (not a state's or an inner part's, as getLayoutCss reads them) include one at this breakpoint (null: by default), each line's `@media` read by `read`. */
|
|
230
|
+
const hasLayoutAt = (component, media, read = (written) => written) => Object.values(component.css.layout).some((lines) => lines.some((line) => read(line.media) === media && isElementDeclaration(line)));
|
|
231
|
+
// ── reporting ──
|
|
232
|
+
/** Runs a check by default, then at each `@media` these components' layout lines use; a breakpoint's note that repeats the default's isn't reported again. */
|
|
233
|
+
function reportAtEachBreakpoint(components, check, { report }) {
|
|
234
|
+
const byDefault = check(null);
|
|
235
|
+
if (byDefault)
|
|
236
|
+
reportNote(byDefault, null, report);
|
|
237
|
+
for (const media of getBreakpoints(components)) {
|
|
238
|
+
const note = check(media);
|
|
239
|
+
if (note && note.text !== byDefault?.text)
|
|
240
|
+
reportNote(note, media, report);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** The `@media` queries these components' own layout lines use, as written, in order of first use. */
|
|
244
|
+
function getBreakpoints(components) {
|
|
245
|
+
const medias = new Set();
|
|
246
|
+
for (const component of components)
|
|
247
|
+
for (const lines of Object.values(component.css.layout))
|
|
248
|
+
for (const line of lines)
|
|
249
|
+
if (line.media !== null && isElementDeclaration(line))
|
|
250
|
+
medias.add(line.media);
|
|
251
|
+
return [...medias];
|
|
252
|
+
}
|
|
253
|
+
/** A note on its component: `layout.css:<line>: <text>[ at @media …] (tracker <row>).` */
|
|
254
|
+
function reportNote({ loc, line, text, row, check }, media, report) {
|
|
255
|
+
report({ check, loc, message: `layout.css:${line}: ${text}${media ? ` at @media ${media}` : ''} (tracker ${row}).` });
|
|
256
|
+
}
|
|
257
|
+
/** A layout line with its value as the import reads it: trimmed, a layout variable `var(--name, <fallback>)` by its fallback. */
|
|
258
|
+
function getLayoutLine(rulesApi, component, property, media) {
|
|
259
|
+
const line = rulesApi.getLayoutCss(component, property, media);
|
|
260
|
+
return line && { ...line, value: getLayoutLiteral(line.value.trim()) };
|
|
261
|
+
}
|
|
262
|
+
/** `display: flex`, the import's stack. */
|
|
263
|
+
const isStack = (display) => display?.value === 'flex';
|
|
264
|
+
/** The components around this element, closest first: wrappers are transparent. */
|
|
265
|
+
function getHolders(element, rulesApi) {
|
|
266
|
+
const holders = [];
|
|
267
|
+
for (let node = element.parent; node; node = node.parent) {
|
|
268
|
+
if (node.type !== 'JSXElement')
|
|
269
|
+
continue;
|
|
270
|
+
const type = rulesApi.getType(node);
|
|
271
|
+
if (type === null || !WRAPPERS.includes(type))
|
|
272
|
+
holders.push(node);
|
|
273
|
+
}
|
|
274
|
+
return holders;
|
|
275
|
+
}
|
|
276
|
+
/** How a message names the element: its first class as a selector, or its key when it has no class (and so no layout CSS). */
|
|
277
|
+
const nameOf = (component) => (component.classNames[0] ? `.${component.classNames[0]}` : component.key);
|
package/dist/rules-api/index.js
CHANGED
|
@@ -3,4 +3,5 @@ export { createRulesApi, getOrCreateRulesApi } from "./rules-api.js";
|
|
|
3
3
|
export { createCssFileReporter, describeAtRule, reportCssProblem } from "./report.js";
|
|
4
4
|
export { bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getParamReadKind, getProperty, isBound, isOneEditAway, isRecord, isReservedAttribute, isStructure, isReadByImport, ownerOf, } from "../shared/utils.js";
|
|
5
5
|
export { BUILDERS, CATALOG_FONTS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, INTERACTIONS_PACKAGES, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, WRAPPERS, } from "../shared/constants.js";
|
|
6
|
-
export {
|
|
6
|
+
export { isElementDeclaration } from "./utils.js";
|
|
7
|
+
export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getLayoutLiteral, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readFontShorthand, readSegment } from "../shared/css.js";
|
|
@@ -6,8 +6,8 @@ import { GLOBAL_COMPONENTS_FOLDER } from "../shared/site-files-tree-mapping.js";
|
|
|
6
6
|
import { models, pageId } from "../bi/stats.js";
|
|
7
7
|
import { readPage } from "../page-model/index.js";
|
|
8
8
|
import { getAttributeElements as getHeldElements, getAttributeValue, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, hasDataAttributes, isTypeScript } from "../shared/utils.js";
|
|
9
|
-
import { readBreakpointName, readCustomMedia, readThemeFontTokens, readThemeTokens } from "../shared/css.js";
|
|
10
|
-
import { WRAPPERS } from "../shared/constants.js";
|
|
9
|
+
import { CONTENT_TARGET, readBreakpointName, readCustomMedia, readThemeFontTokens, readThemeTokens } from "../shared/css.js";
|
|
10
|
+
import { LAYER_ATTRIBUTES, WRAPPERS } from "../shared/constants.js";
|
|
11
11
|
import { getEnclosingAtRules, isBaseDeclaration } from "./utils.js";
|
|
12
12
|
/** Theme lines come from @apply inside style.css, so style.css's own lines usually come after them. */
|
|
13
13
|
const CASCADE_ORDER = ['layout', 'theme', 'style'];
|
|
@@ -26,6 +26,10 @@ export function createRulesApi(page) {
|
|
|
26
26
|
return isWrapper(child) ? getChildElements(child) : [child];
|
|
27
27
|
});
|
|
28
28
|
const getAttributeElements = (node) => getHeldElements(node).flatMap((held) => (isWrapper(held.element) ? getChildElements(held.element).map((inner) => ({ ...held, element: inner })) : [held]));
|
|
29
|
+
const getLayoutCss = (component, property, media) => {
|
|
30
|
+
const lines = (component.css.layout[property] ?? []).filter((line) => line.selector === '&' || line.selector === `& @origami-target ${CONTENT_TARGET}`);
|
|
31
|
+
return lines.filter((line) => line.media === media).at(-1) ?? (media === null ? undefined : lines.filter((line) => line.media === null).at(-1));
|
|
32
|
+
};
|
|
29
33
|
return {
|
|
30
34
|
page,
|
|
31
35
|
// A JSX element starts where its component starts.
|
|
@@ -34,6 +38,7 @@ export function createRulesApi(page) {
|
|
|
34
38
|
getRoot: () => Object.values(page.components).find((component) => !getParent(component)) ?? null,
|
|
35
39
|
getCss,
|
|
36
40
|
getBaseCss: (component, property) => getCss(component, property).filter(isBaseDeclaration).at(-1),
|
|
41
|
+
getLayoutCss,
|
|
37
42
|
getThemeTokens: () => readThemeTokens(page.css.files.theme),
|
|
38
43
|
getThemeFontTokens: () => readThemeFontTokens(page.css.files.theme),
|
|
39
44
|
getCustomMedia: (kind) => readCustomMedia(page.css.files[kind]),
|
|
@@ -46,6 +51,7 @@ export function createRulesApi(page) {
|
|
|
46
51
|
getChildren,
|
|
47
52
|
getChildElements,
|
|
48
53
|
getAttributeElements,
|
|
54
|
+
getItemElements: (node) => [...getChildElements(node), ...getAttributeElements(node).filter(({ name }) => !LAYER_ATTRIBUTES.includes(name)).map(({ element }) => element)],
|
|
49
55
|
getAttributeValue,
|
|
50
56
|
getSlot,
|
|
51
57
|
getImport: (node) => getImport(node.type === 'JSXElement' ? getTagName(node).split('.')[0] : node.name, getProgram(node)),
|
package/dist/rules-api/utils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** On the component itself, outside any @media and @origami-target: applies by default. */
|
|
2
2
|
export const isBaseDeclaration = (decl) => !decl.media && decl.selector === '&';
|
|
3
|
+
/** On the component itself (`&`) or in one of its `@origami-target` blocks, at any breakpoint: the lines the import merges into the element's layout scope; not a state's or an inner part's. */
|
|
4
|
+
export const isElementDeclaration = (decl) => decl.selector === '&' || decl.selector.startsWith('& @origami-target ');
|
|
3
5
|
/** The at-rules around a CSS node, outer to inner (`@media`, `@scope`, `@layer`, `@mixin`…); [] at the file's top level. */
|
|
4
6
|
export function getEnclosingAtRules(node) {
|
|
5
7
|
const atRules = [];
|
package/dist/shared/css.js
CHANGED
|
@@ -52,6 +52,11 @@ function parseCustomMedia(params) {
|
|
|
52
52
|
const [, name = '', query = ''] = /^(\S*)\s*([\s\S]*)$/.exec(params.trim()) ?? [];
|
|
53
53
|
return { name, query };
|
|
54
54
|
}
|
|
55
|
+
/** A layout value as the import reads it (layoutVariables.ts literalOf): `var(--name, <fallback>)`, a field connected to one of the site's layout
|
|
56
|
+
* variables, by its fallback; any other value as is. */
|
|
57
|
+
export const getLayoutLiteral = (value) => LAYOUT_VARIABLE_TOKEN.exec(value)?.[1] ?? value;
|
|
58
|
+
/** `var(--name, <fallback>)`: a field connected to one of the site's layout variables, written with the field's own value as the fallback. */
|
|
59
|
+
const LAYOUT_VARIABLE_TOKEN = /^var\(--[A-Za-z_-][\w-]*,\s*(.+)\)$/;
|
|
55
60
|
/** A `font` shorthand's weight and family; undefined for a `var(…)` or a value with no family after its size. */
|
|
56
61
|
export function readFontShorthand(value) {
|
|
57
62
|
if (value.includes('var('))
|
package/dist/shared/utils.js
CHANGED
|
@@ -9,6 +9,10 @@ const TYPE_BY_IMPORT_SOURCE = [
|
|
|
9
9
|
[new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?HeaderSection$`), 'header'],
|
|
10
10
|
[new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?FooterSection$`), 'footer'],
|
|
11
11
|
[new RegExp(`^${SYSTEM_PACKAGE}/InternalContainer$`), 'innerElement'],
|
|
12
|
+
// The editor's containers and its background, as the export imports their wrappers.
|
|
13
|
+
[/^@origami-wrappers\/wixEditorElements\/boxcontainer$/, 'box'],
|
|
14
|
+
[/^@origami-wrappers\/wixEditorElements\/menu$/, 'menu'],
|
|
15
|
+
[/^@origami-wrappers\/wixSystemElements\/background$/, 'background'],
|
|
12
16
|
// A global component's folder is `<Type>_<id>`: HeaderSection_mb7ogqqn.
|
|
13
17
|
[new RegExp(`/${GLOBAL_COMPONENTS}/(?:HeaderSection|Header)_[^/]+$`), 'globalHeader'],
|
|
14
18
|
[new RegExp(`/${GLOBAL_COMPONENTS}/(?:FooterSection|Footer)_[^/]+$`), 'globalFooter'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-lint",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.33",
|
|
4
4
|
"description": "Offline lint for an exported Origami site: lints the pages that changed since the last passing run",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
]
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
|
-
"falconPackageHash": "
|
|
55
|
+
"falconPackageHash": "fa4525b9f8f5fa63ad7d72b383deab33b1a9c16033290d6381eb0ee9"
|
|
56
56
|
}
|