@wix/origami-lint 1.34.28 → 1.34.29

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.
@@ -12,8 +12,9 @@ import pageStructure from "./site-app/page-structure.js";
12
12
  import slotAttributes from "./site-app/slot-attributes.js";
13
13
  import slotPlacement from "./site-app/slot-placement.js";
14
14
  import themeFontIds from "./site-app/theme-font-ids.js";
15
+ import themeFontKnown from "./site-app/theme-font-known.js";
15
16
  import themeRoot from "./site-app/theme-root.js";
16
17
  import noFreeCode from "./site-app/no-free-code.js";
17
18
  // Execution registries combine already-checked rules; only this boundary erases their individual key unions.
18
- export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement];
19
+ export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement];
19
20
  export { defineRule } from "./site-app/define-rule.js";
@@ -1,71 +1,29 @@
1
1
  // Every theme font token (`--wst-heading-1-font: normal normal 700 --spx(88)/1em benzin`) must end in exactly one font id
2
2
  // from the Wix font catalog in @wix/origami-fonts: no quotes, commas or fallbacks, the same case. The editor reads that
3
- // family as one Wix font id and fails the import otherwise. A legacy font, which the editor no longer offers, counts as
4
- // not in the catalog. A `var(…)` token is left alone.
3
+ // family as one Wix font id. The import keeps any family as written (an unknown id, a quoted one, a list, another case),
4
+ // so this is a warning. A legacy font is in the catalog, and the import takes it. A `var(…)` token is left alone.
5
5
  // theme.css isn't a file oxlint lints, so the rule runs on each linted page (a theme change lints every page) and
6
6
  // marks the page's start; the message names the token's line in theme.css.
7
- import { fonts } from '@wix/origami-fonts';
8
- import { reportCssProblem } from "../../rules-api/index.js";
7
+ import { CATALOG_FONTS, readFontShorthand, reportCssProblem } from "../../rules-api/index.js";
9
8
  import { defineRule } from "./define-rule.js";
10
9
  const CHECKS = ['invalid_font_id'];
11
- const FONT_TOKEN = /^--wst-[\w-]+-font$/;
12
- // The size in a `font` shorthand, with its optional line height: the family follows it. The number's quantifiers
13
- // never overlap, so a long run of digits that is not a size fails in linear time.
14
- const SIZE = /^(?:--spx\(.*\)|calc\(.*\)|(?:\d+(?:\.\d*)?|\.\d+)(?:px|em|rem|%|vw|vh|pt)?)(?:\/\S+)?$/i;
15
- const FONT_IDS = new Set(fonts.filter((font) => font.permissions !== 'legacy').map((font) => font.id));
16
10
  export default defineRule({
17
11
  name: 'theme-font-ids',
18
12
  checks: CHECKS,
19
13
  editors: 'all',
14
+ severity: 'warn',
20
15
  callbacks: {
21
16
  Program: (input) => {
22
- for (const token of getFontTokens(input))
17
+ for (const token of input.rulesApi.getThemeFontTokens())
23
18
  hasCatalogFontId(token, input);
24
19
  },
25
20
  },
26
21
  });
27
22
  // ── the steps ──
28
- /** The theme's font tokens: `--wst-…-font`. */
29
- function getFontTokens({ rulesApi }) {
30
- return rulesApi.getThemeTokens().filter(({ name }) => FONT_TOKEN.test(name));
31
- }
32
23
  /** Reports a font token whose family isn't exactly one id from the catalog. A `var(…)` or a value without a family isn't checked. */
33
24
  function hasCatalogFontId({ name, value, line }, { report }) {
34
- const family = getFontFamily(value);
35
- if (family === undefined || FONT_IDS.has(family))
25
+ const family = readFontShorthand(value)?.family;
26
+ if (family === undefined || CATALOG_FONTS.has(family))
36
27
  return;
37
28
  reportCssProblem(report, 'theme.css', line, 'invalid_font_id', `${name} must name exactly one Wix font id from the catalog; found ${family}. Run \`wix origami search-font --font <name>\` for the id.`);
38
29
  }
39
- // ── reading a font value ──
40
- /** A value split on top-level whitespace, so `--spx(16)/1.3em` stays one part. */
41
- function splitParts(value) {
42
- const parts = [];
43
- let depth = 0;
44
- let current = '';
45
- for (const char of value) {
46
- if (char === '(')
47
- depth++;
48
- else if (char === ')')
49
- depth--;
50
- if (depth === 0 && /\s/.test(char)) {
51
- if (current)
52
- parts.push(current);
53
- current = '';
54
- }
55
- else
56
- current += char;
57
- }
58
- if (current)
59
- parts.push(current);
60
- return parts;
61
- }
62
- /** A font shorthand's family as written, after its last size; undefined for `var(…)` or no family. */
63
- function getFontFamily(value) {
64
- if (value.includes('var('))
65
- return undefined;
66
- const parts = splitParts(value);
67
- const size = parts.findLastIndex((part) => SIZE.test(part));
68
- if (size === -1 || size === parts.length - 1)
69
- return undefined;
70
- return parts.slice(size + 1).join(' ');
71
- }
@@ -0,0 +1,30 @@
1
+ // A theme font token (`--wst-heading-1-font: normal normal 700 --spx(88)/1em benzin`) names a weight its font has, in
2
+ // the Wix font catalog's weights (@wix/origami-fonts); tracker row KIT-fontCatalog. The import keeps the value as written,
3
+ // so a weight the font lacks only renders worse (the browser synthesizes it): a warning. A font that isn't a catalog id is
4
+ // theme-font-ids' warning, not checked again here; nor is a `var(…)`, or `bolder` / `lighter`, which depend on the parent.
5
+ import { CATALOG_FONTS, readFontShorthand, reportCssProblem } from "../../rules-api/index.js";
6
+ import { defineRule } from "./define-rule.js";
7
+ const CHECKS = ['missing_font_weight'];
8
+ export default defineRule({
9
+ name: 'theme-font-known',
10
+ checks: CHECKS,
11
+ editors: ['studio2'],
12
+ severity: 'warn',
13
+ callbacks: {
14
+ Program: checkThemeFontWeights,
15
+ },
16
+ });
17
+ // ── the steps ──
18
+ /** Each theme font token's weight. */
19
+ function checkThemeFontWeights(input) {
20
+ for (const token of input.rulesApi.getThemeFontTokens())
21
+ checkFontTokenWeight(token, input);
22
+ }
23
+ /** Reports a font token whose weight isn't one of its catalog font's weights. */
24
+ function checkFontTokenWeight({ name, value, line }, { report }) {
25
+ const font = readFontShorthand(value);
26
+ const weights = font && CATALOG_FONTS.get(font.family)?.weights;
27
+ if (!font || font.weight === undefined || !weights || weights.includes(font.weight))
28
+ return;
29
+ reportCssProblem(report, 'theme.css', line, 'missing_font_weight', `${name} uses weight ${font.weight}, which ${font.family} does not have; the browser will synthesize it. Write one of its weights: ${weights.join(', ')} (rule KIT-fontCatalog).`);
30
+ }
@@ -2,5 +2,5 @@
2
2
  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
- export { BUILDERS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, 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 { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readSegment } from "../shared/css.js";
5
+ export { BUILDERS, CATALOG_FONTS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, 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 { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readFontShorthand, readSegment } from "../shared/css.js";
@@ -6,7 +6,7 @@ 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, readThemeTokens } from "../shared/css.js";
9
+ import { readBreakpointName, readCustomMedia, readThemeFontTokens, readThemeTokens } from "../shared/css.js";
10
10
  import { 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. */
@@ -35,6 +35,7 @@ export function createRulesApi(page) {
35
35
  getCss,
36
36
  getBaseCss: (component, property) => getCss(component, property).filter(isBaseDeclaration).at(-1),
37
37
  getThemeTokens: () => readThemeTokens(page.css.files.theme),
38
+ getThemeFontTokens: () => readThemeFontTokens(page.css.files.theme),
38
39
  getCustomMedia: (kind) => readCustomMedia(page.css.files[kind]),
39
40
  getBreakpointName: readBreakpointName,
40
41
  getEnclosingAtRules,
@@ -1,3 +1,7 @@
1
+ // The constants more than one part reads: the groups of element types, the packages' names, and what the converter reserves on a tag (reservedAttributes.ts). A rule imports them from rules-api/index.ts; a part from here.
2
+ import { fonts } from '@wix/origami-fonts';
3
+ /** The Wix font catalog (@wix/origami-fonts) by id, legacy fonts too: the import takes a legacy font as it takes any other. */
4
+ export const CATALOG_FONTS = new Map(fonts.map((font) => [font.id, font]));
1
5
  /** Any section: rules that don't care which one. */
2
6
  export const SECTIONS = ['section', 'header', 'footer'];
3
7
  /** A page's reference to a global component (`<HeaderSection_x sharedCompId="…" />`). */
@@ -18,6 +18,13 @@ const BASE_CLASS = /^\.[^:[\]"]+/;
18
18
  const FUNCTIONAL_PSEUDO = /^:[A-Za-z-]+\([^)]*\)/;
19
19
  /** A class after the first: a state (`&.onSale`). */
20
20
  const STATE_CLASS = /^\.[^.:[\]"]+/;
21
+ /** A theme font token: `--wst-heading-1-font`. */
22
+ const THEME_FONT_TOKEN = /^--wst-[\w-]+-font$/;
23
+ // The size in a `font` shorthand, with its optional line height: the family follows it. The number's quantifiers
24
+ // never overlap, so a long run of digits that is not a size fails in linear time.
25
+ const FONT_SIZE = /^(?:--spx\(.*\)|calc\(.*\)|(?:\d+(?:\.\d*)?|\.\d+)(?:px|em|rem|%|vw|vh|pt)?)(?:\/\S+)?$/i;
26
+ /** A number weight in a `font` shorthand: before the size, only the weight is a bare number. */
27
+ const FONT_WEIGHT = /^\d+(?:\.\d+)?$/;
21
28
  const PSEUDO_CLASS = /^:[A-Za-z-]+/;
22
29
  const ATTRIBUTE = /^\[[^\]]*\]/;
23
30
  /** Every `--name: value` in a file, wherever it's declared, in order. No file → none. */
@@ -28,6 +35,8 @@ export function readThemeTokens(file) {
28
35
  });
29
36
  return tokens;
30
37
  }
38
+ /** The theme's font tokens (`--wst-heading-1-font: …`), as readThemeTokens reads them. */
39
+ export const readThemeFontTokens = (file) => readThemeTokens(file).filter(({ name }) => THEME_FONT_TOKEN.test(name));
31
40
  /** Every `@custom-media --name (query)` at a file's top level, in order, as the import reads them; theme.css's are the site's breakpoints. No file → none. */
32
41
  export function readCustomMedia(file) {
33
42
  if (file === undefined)
@@ -43,6 +52,51 @@ function parseCustomMedia(params) {
43
52
  const [, name = '', query = ''] = /^(\S*)\s*([\s\S]*)$/.exec(params.trim()) ?? [];
44
53
  return { name, query };
45
54
  }
55
+ /** A `font` shorthand's weight and family; undefined for a `var(…)` or a value with no family after its size. */
56
+ export function readFontShorthand(value) {
57
+ if (value.includes('var('))
58
+ return undefined;
59
+ const parts = splitFontParts(value);
60
+ const size = parts.findLastIndex((part) => FONT_SIZE.test(part));
61
+ if (size === -1 || size === parts.length - 1)
62
+ return undefined;
63
+ return { weight: readFontWeight(parts.slice(0, size)), family: parts.slice(size + 1).join(' ') };
64
+ }
65
+ /** The weight among a shorthand's parts before its size: a number, `bold`, or 400 (`normal`, or none written). */
66
+ function readFontWeight(parts) {
67
+ for (const part of parts) {
68
+ if (FONT_WEIGHT.test(part))
69
+ return Number(part);
70
+ const keyword = part.toLowerCase();
71
+ if (keyword === 'bold')
72
+ return 700;
73
+ if (keyword === 'bolder' || keyword === 'lighter')
74
+ return undefined;
75
+ }
76
+ return 400;
77
+ }
78
+ /** A value split on top-level whitespace, so `--spx(16)/1.3em` stays one part. */
79
+ function splitFontParts(value) {
80
+ const parts = [];
81
+ let depth = 0;
82
+ let current = '';
83
+ for (const char of value) {
84
+ if (char === '(')
85
+ depth++;
86
+ else if (char === ')')
87
+ depth--;
88
+ if (depth === 0 && /\s/.test(char)) {
89
+ if (current)
90
+ parts.push(current);
91
+ current = '';
92
+ }
93
+ else
94
+ current += char;
95
+ }
96
+ if (current)
97
+ parts.push(current);
98
+ return parts;
99
+ }
46
100
  /** A container's children, comments aside; none for a node with no block. */
47
101
  export const getCssChildren = (node) => (node.nodes ?? []).filter((child) => child.type !== 'comment');
48
102
  /** Whether a property picks a preset (css/utils.ts `isPresetProperty`): `--preset` (the rule's own element) or `--preset-<part path>`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.28",
3
+ "version": "1.34.29",
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": "97fa36a3eb31cb9ae9a81f66fff2679fde030431f3b1767dcaf5154a"
55
+ "falconPackageHash": "2c36ab85acce27a2dc10d53da990328e863a2935e38f2d82a6dd7467"
56
56
  }