@hyperframes/lint 0.9.12 → 0.9.14
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/browser.js +109 -10
- package/dist/index.d.ts +8 -1
- package/dist/index.js +110 -10
- package/package.json +2 -2
package/dist/browser.js
CHANGED
|
@@ -1824,6 +1824,71 @@ function findLinkedClipFindings(ctx) {
|
|
|
1824
1824
|
return findings;
|
|
1825
1825
|
}
|
|
1826
1826
|
|
|
1827
|
+
// src/rules/audioAttributes.ts
|
|
1828
|
+
var AUDIO_ATTRIBUTES = {
|
|
1829
|
+
"data-automation": { list: "lanes", noun: "automation", item: "Lane" },
|
|
1830
|
+
"data-fx-chain": { list: "nodes", noun: "chain", item: "Node" }
|
|
1831
|
+
};
|
|
1832
|
+
var AUDIO_ATTRIBUTE_VERSION = 1;
|
|
1833
|
+
function audioAttributeShapeError(attr, json) {
|
|
1834
|
+
const { list, noun, item } = AUDIO_ATTRIBUTES[attr];
|
|
1835
|
+
const parsed = parseEnvelope(json);
|
|
1836
|
+
if (typeof parsed === "string") return parsed;
|
|
1837
|
+
if (parsed.version !== AUDIO_ATTRIBUTE_VERSION) {
|
|
1838
|
+
return `unsupported ${noun} version: ${String(parsed.version)}`;
|
|
1839
|
+
}
|
|
1840
|
+
const entries = parsed[list];
|
|
1841
|
+
if (!Array.isArray(entries)) return `it is missing a \`${list}\` array`;
|
|
1842
|
+
for (const [i, entry] of entries.entries()) {
|
|
1843
|
+
const reason = entryShapeError(attr, item, i, entry);
|
|
1844
|
+
if (reason) return reason;
|
|
1845
|
+
}
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
function parseEnvelope(json) {
|
|
1849
|
+
let raw;
|
|
1850
|
+
try {
|
|
1851
|
+
raw = JSON.parse(json);
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
return `it is not valid JSON (${err.message})`;
|
|
1854
|
+
}
|
|
1855
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
1856
|
+
return "it is not a JSON object";
|
|
1857
|
+
return raw;
|
|
1858
|
+
}
|
|
1859
|
+
function entryShapeError(attr, item, i, entry) {
|
|
1860
|
+
if (typeof entry !== "object" || entry === null) return `${item} ${i} is not an object`;
|
|
1861
|
+
const fields = entry;
|
|
1862
|
+
if (attr === "data-fx-chain") {
|
|
1863
|
+
return typeof fields.type === "string" ? null : `Node ${i} has no effect \`type\``;
|
|
1864
|
+
}
|
|
1865
|
+
if (typeof fields.target !== "string") return `Lane ${i} has no \`target\``;
|
|
1866
|
+
if (!Array.isArray(fields.points)) return `Lane ${i} is missing a \`points\` array`;
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
function findUnreadableAudioAttributeFindings(ctx) {
|
|
1870
|
+
const findings = [];
|
|
1871
|
+
for (const tag of ctx.tags) {
|
|
1872
|
+
for (const attr of Object.keys(AUDIO_ATTRIBUTES)) {
|
|
1873
|
+
const value = readDecodedAttr(tag.raw, attr);
|
|
1874
|
+
if (value === null || value === "") continue;
|
|
1875
|
+
const reason = audioAttributeShapeError(attr, value);
|
|
1876
|
+
if (!reason) continue;
|
|
1877
|
+
const id = readAttr(tag.raw, "id");
|
|
1878
|
+
const { list } = AUDIO_ATTRIBUTES[attr];
|
|
1879
|
+
findings.push({
|
|
1880
|
+
code: "audio_attribute_unreadable",
|
|
1881
|
+
severity: "error",
|
|
1882
|
+
message: `${id ? `#${id}` : `<${tag.name}>`} has a \`${attr}\` the render cannot read: ${reason}. The render fails on it, locally and in the cloud.`,
|
|
1883
|
+
elementId: id || void 0,
|
|
1884
|
+
fixHint: `Write it as {"version": ${AUDIO_ATTRIBUTE_VERSION}, "${list}": [...]}, or remove the attribute.`,
|
|
1885
|
+
snippet: truncateSnippet(tag.raw)
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return findings;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1827
1892
|
// src/rules/media.ts
|
|
1828
1893
|
function tweensVolumeInSameCall(script, id) {
|
|
1829
1894
|
const selector = new RegExp(`#${escapeRegExp(id)}(?![\\w-])`, "g");
|
|
@@ -2556,7 +2621,8 @@ var mediaRules = [
|
|
|
2556
2621
|
findAudioGroupCarveAttrFindings,
|
|
2557
2622
|
// audio_group_holds_elements
|
|
2558
2623
|
findAudioGroupHoldsElementsFindings,
|
|
2559
|
-
findLinkedClipFindings
|
|
2624
|
+
findLinkedClipFindings,
|
|
2625
|
+
findUnreadableAudioAttributeFindings
|
|
2560
2626
|
];
|
|
2561
2627
|
function findVolumeTweenOverridesGainFindings(ctx) {
|
|
2562
2628
|
const boosted = ctx.tags.filter((tag) => isMediaTag(tag.name)).map((tag) => ({ tag, volume: Number(readAttr(tag.raw, "data-volume") ?? "1") })).filter((entry) => Number.isFinite(entry.volume) && entry.volume !== 1).filter((entry) => !readDecodedAttr(entry.tag.raw, "data-automation")).map((entry) => ({ ...entry, id: readAttr(entry.tag.raw, "id") })).filter((entry) => Boolean(entry.id));
|
|
@@ -6420,7 +6486,11 @@ var textureRules = [
|
|
|
6420
6486
|
|
|
6421
6487
|
// src/rules/fonts.ts
|
|
6422
6488
|
import postcss3 from "postcss";
|
|
6423
|
-
import {
|
|
6489
|
+
import {
|
|
6490
|
+
FONT_ALIAS_KEYS,
|
|
6491
|
+
parseFontShorthand,
|
|
6492
|
+
resolveAliasDisplayName
|
|
6493
|
+
} from "@hyperframes/parsers/composition";
|
|
6424
6494
|
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
6425
6495
|
"serif",
|
|
6426
6496
|
"sans-serif",
|
|
@@ -6503,14 +6573,20 @@ function resolveUsedFontNames(value, properties, resolving = /* @__PURE__ */ new
|
|
|
6503
6573
|
return resolveUsedFontNames(referenced, properties, new Set(resolving).add(property));
|
|
6504
6574
|
});
|
|
6505
6575
|
}
|
|
6576
|
+
function usedFamilyList(decl) {
|
|
6577
|
+
if (isFontFaceDeclaration(decl)) return "";
|
|
6578
|
+
if (decl.prop.toLowerCase() !== "font") return decl.value;
|
|
6579
|
+
const shorthand = parseFontShorthand(decl.value);
|
|
6580
|
+
return shorthand.certain ? shorthand.families : "";
|
|
6581
|
+
}
|
|
6582
|
+
var FONT_FAMILY_PROPS = /^font(?:-family)?$/i;
|
|
6506
6583
|
function extractUsedFontFamilies(styles) {
|
|
6507
6584
|
const used = [];
|
|
6508
6585
|
const seen = /* @__PURE__ */ new Set();
|
|
6509
6586
|
const properties = collectFontCustomProperties(styles);
|
|
6510
6587
|
for (const style of styles) {
|
|
6511
|
-
parseFontCss(style.content)?.walkDecls(
|
|
6512
|
-
|
|
6513
|
-
for (const name of resolveUsedFontNames(decl.value, properties)) {
|
|
6588
|
+
parseFontCss(style.content)?.walkDecls(FONT_FAMILY_PROPS, (decl) => {
|
|
6589
|
+
for (const name of resolveUsedFontNames(usedFamilyList(decl), properties)) {
|
|
6514
6590
|
if (!GENERIC_FAMILIES.has(name) && !seen.has(name)) {
|
|
6515
6591
|
seen.add(name);
|
|
6516
6592
|
used.push(name);
|
|
@@ -6612,9 +6688,33 @@ var fontRules = [
|
|
|
6612
6688
|
code: "font_family_without_font_face",
|
|
6613
6689
|
severity: "error",
|
|
6614
6690
|
message: `Font ${undeclared.length === 1 ? "family" : "families"} used without @font-face declaration: ${undeclared.join(", ")}. These are not in the auto-resolved font list, so the renderer cannot supply them automatically. Text will fall back to a generic font, producing incorrect typography in the video.`,
|
|
6615
|
-
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files
|
|
6691
|
+
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files, or switch to a Google Fonts family. An OS font such as PingFang SC or Hiragino Sans renders locally from the installed copy, but a cloud render has no copy and refuses it."
|
|
6616
6692
|
});
|
|
6617
6693
|
return findings;
|
|
6694
|
+
},
|
|
6695
|
+
({ styles, rawSource, options, locate }) => {
|
|
6696
|
+
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
6697
|
+
const findings = [];
|
|
6698
|
+
for (const style of styles) {
|
|
6699
|
+
parseFontCss(style.content)?.walkAtRules(/^font-face$/i, (rule) => {
|
|
6700
|
+
let family = null;
|
|
6701
|
+
let sources = "";
|
|
6702
|
+
rule.walkDecls((decl) => {
|
|
6703
|
+
const prop = decl.prop.toLowerCase();
|
|
6704
|
+
if (prop === "font-family") family = normalizeUsedFontName(decl.value);
|
|
6705
|
+
else if (prop === "src") sources += ` ${decl.value}`;
|
|
6706
|
+
});
|
|
6707
|
+
if (!family || !/\blocal\s*\(/i.test(sources) || /\burl\s*\(/i.test(sources)) return;
|
|
6708
|
+
findings.push({
|
|
6709
|
+
...locate(style, rule.source?.start?.offset),
|
|
6710
|
+
code: "font_face_local_only",
|
|
6711
|
+
severity: "warning",
|
|
6712
|
+
message: `@font-face '${family}' only names a font installed on this machine (src: local()). A local render uses the installed copy; a cloud render or another machine has none and falls back to a different font.`,
|
|
6713
|
+
fixHint: "Add a url() source to the font file (.woff2) inside the project, or use a Google Fonts family, so every render draws the same glyphs."
|
|
6714
|
+
});
|
|
6715
|
+
});
|
|
6716
|
+
}
|
|
6717
|
+
return findings;
|
|
6618
6718
|
}
|
|
6619
6719
|
];
|
|
6620
6720
|
function uniqueFontLocation(styles, families, locate) {
|
|
@@ -6622,10 +6722,9 @@ function uniqueFontLocation(styles, families, locate) {
|
|
|
6622
6722
|
const properties = collectFontCustomProperties(styles);
|
|
6623
6723
|
for (const style of styles) {
|
|
6624
6724
|
try {
|
|
6625
|
-
postcss3.parse(style.content).walkDecls(
|
|
6626
|
-
|
|
6627
|
-
if (!
|
|
6628
|
-
return;
|
|
6725
|
+
postcss3.parse(style.content).walkDecls(FONT_FAMILY_PROPS, (decl) => {
|
|
6726
|
+
const named = resolveUsedFontNames(usedFamilyList(decl), properties);
|
|
6727
|
+
if (!named.some((name) => families.includes(name))) return;
|
|
6629
6728
|
locations.push(locate(style, decl.source?.start?.offset));
|
|
6630
6729
|
});
|
|
6631
6730
|
} catch {
|
package/dist/index.d.ts
CHANGED
|
@@ -109,4 +109,11 @@ interface ProjectLintResult {
|
|
|
109
109
|
declare function collectExternalStyles(projectDir: string, html: string, compSrcPath?: string): NonNullable<HyperframeLinterOptions["externalStyles"]>;
|
|
110
110
|
declare function lintProject(projectDir: string, entryFile?: string, hostOptions?: Pick<HyperframeLinterOptions, "host" | "isSubComposition">): Promise<ProjectLintResult>;
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
type AudioAttribute = "data-automation" | "data-fx-chain";
|
|
113
|
+
/**
|
|
114
|
+
* Why the renderer would refuse an audio attribute's value before reading any lane or effect, or null when the
|
|
115
|
+
* envelope reads. Lane targets and effect types are left to the render: they need core's effect registry.
|
|
116
|
+
*/
|
|
117
|
+
declare function audioAttributeShapeError(attr: AudioAttribute, json: string): string | null;
|
|
118
|
+
|
|
119
|
+
export { type AudioAttribute, type HyperframeLintFinding, type HyperframeLintResult, type HyperframeLintSeverity, type HyperframeLinterOptions, LINT_RULE_COUNT, LINT_RULE_GROUP_COUNTS, type LintTimings, type ProjectLintResult, audioAttributeShapeError, collectExternalStyles, lintHyperframeHtml, lintMediaUrls, lintProject, shouldBlockRender };
|
package/dist/index.js
CHANGED
|
@@ -1830,6 +1830,71 @@ function findLinkedClipFindings(ctx) {
|
|
|
1830
1830
|
return findings;
|
|
1831
1831
|
}
|
|
1832
1832
|
|
|
1833
|
+
// src/rules/audioAttributes.ts
|
|
1834
|
+
var AUDIO_ATTRIBUTES = {
|
|
1835
|
+
"data-automation": { list: "lanes", noun: "automation", item: "Lane" },
|
|
1836
|
+
"data-fx-chain": { list: "nodes", noun: "chain", item: "Node" }
|
|
1837
|
+
};
|
|
1838
|
+
var AUDIO_ATTRIBUTE_VERSION = 1;
|
|
1839
|
+
function audioAttributeShapeError(attr, json) {
|
|
1840
|
+
const { list, noun, item } = AUDIO_ATTRIBUTES[attr];
|
|
1841
|
+
const parsed = parseEnvelope(json);
|
|
1842
|
+
if (typeof parsed === "string") return parsed;
|
|
1843
|
+
if (parsed.version !== AUDIO_ATTRIBUTE_VERSION) {
|
|
1844
|
+
return `unsupported ${noun} version: ${String(parsed.version)}`;
|
|
1845
|
+
}
|
|
1846
|
+
const entries = parsed[list];
|
|
1847
|
+
if (!Array.isArray(entries)) return `it is missing a \`${list}\` array`;
|
|
1848
|
+
for (const [i, entry] of entries.entries()) {
|
|
1849
|
+
const reason = entryShapeError(attr, item, i, entry);
|
|
1850
|
+
if (reason) return reason;
|
|
1851
|
+
}
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
function parseEnvelope(json) {
|
|
1855
|
+
let raw;
|
|
1856
|
+
try {
|
|
1857
|
+
raw = JSON.parse(json);
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
return `it is not valid JSON (${err.message})`;
|
|
1860
|
+
}
|
|
1861
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
1862
|
+
return "it is not a JSON object";
|
|
1863
|
+
return raw;
|
|
1864
|
+
}
|
|
1865
|
+
function entryShapeError(attr, item, i, entry) {
|
|
1866
|
+
if (typeof entry !== "object" || entry === null) return `${item} ${i} is not an object`;
|
|
1867
|
+
const fields = entry;
|
|
1868
|
+
if (attr === "data-fx-chain") {
|
|
1869
|
+
return typeof fields.type === "string" ? null : `Node ${i} has no effect \`type\``;
|
|
1870
|
+
}
|
|
1871
|
+
if (typeof fields.target !== "string") return `Lane ${i} has no \`target\``;
|
|
1872
|
+
if (!Array.isArray(fields.points)) return `Lane ${i} is missing a \`points\` array`;
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
function findUnreadableAudioAttributeFindings(ctx) {
|
|
1876
|
+
const findings = [];
|
|
1877
|
+
for (const tag of ctx.tags) {
|
|
1878
|
+
for (const attr of Object.keys(AUDIO_ATTRIBUTES)) {
|
|
1879
|
+
const value = readDecodedAttr(tag.raw, attr);
|
|
1880
|
+
if (value === null || value === "") continue;
|
|
1881
|
+
const reason = audioAttributeShapeError(attr, value);
|
|
1882
|
+
if (!reason) continue;
|
|
1883
|
+
const id = readAttr(tag.raw, "id");
|
|
1884
|
+
const { list } = AUDIO_ATTRIBUTES[attr];
|
|
1885
|
+
findings.push({
|
|
1886
|
+
code: "audio_attribute_unreadable",
|
|
1887
|
+
severity: "error",
|
|
1888
|
+
message: `${id ? `#${id}` : `<${tag.name}>`} has a \`${attr}\` the render cannot read: ${reason}. The render fails on it, locally and in the cloud.`,
|
|
1889
|
+
elementId: id || void 0,
|
|
1890
|
+
fixHint: `Write it as {"version": ${AUDIO_ATTRIBUTE_VERSION}, "${list}": [...]}, or remove the attribute.`,
|
|
1891
|
+
snippet: truncateSnippet(tag.raw)
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return findings;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1833
1898
|
// src/rules/media.ts
|
|
1834
1899
|
function tweensVolumeInSameCall(script, id) {
|
|
1835
1900
|
const selector = new RegExp(`#${escapeRegExp(id)}(?![\\w-])`, "g");
|
|
@@ -2562,7 +2627,8 @@ var mediaRules = [
|
|
|
2562
2627
|
findAudioGroupCarveAttrFindings,
|
|
2563
2628
|
// audio_group_holds_elements
|
|
2564
2629
|
findAudioGroupHoldsElementsFindings,
|
|
2565
|
-
findLinkedClipFindings
|
|
2630
|
+
findLinkedClipFindings,
|
|
2631
|
+
findUnreadableAudioAttributeFindings
|
|
2566
2632
|
];
|
|
2567
2633
|
function findVolumeTweenOverridesGainFindings(ctx) {
|
|
2568
2634
|
const boosted = ctx.tags.filter((tag) => isMediaTag(tag.name)).map((tag) => ({ tag, volume: Number(readAttr(tag.raw, "data-volume") ?? "1") })).filter((entry) => Number.isFinite(entry.volume) && entry.volume !== 1).filter((entry) => !readDecodedAttr(entry.tag.raw, "data-automation")).map((entry) => ({ ...entry, id: readAttr(entry.tag.raw, "id") })).filter((entry) => Boolean(entry.id));
|
|
@@ -6426,7 +6492,11 @@ var textureRules = [
|
|
|
6426
6492
|
|
|
6427
6493
|
// src/rules/fonts.ts
|
|
6428
6494
|
import postcss3 from "postcss";
|
|
6429
|
-
import {
|
|
6495
|
+
import {
|
|
6496
|
+
FONT_ALIAS_KEYS,
|
|
6497
|
+
parseFontShorthand,
|
|
6498
|
+
resolveAliasDisplayName
|
|
6499
|
+
} from "@hyperframes/parsers/composition";
|
|
6430
6500
|
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
6431
6501
|
"serif",
|
|
6432
6502
|
"sans-serif",
|
|
@@ -6509,14 +6579,20 @@ function resolveUsedFontNames(value, properties, resolving = /* @__PURE__ */ new
|
|
|
6509
6579
|
return resolveUsedFontNames(referenced, properties, new Set(resolving).add(property));
|
|
6510
6580
|
});
|
|
6511
6581
|
}
|
|
6582
|
+
function usedFamilyList(decl) {
|
|
6583
|
+
if (isFontFaceDeclaration(decl)) return "";
|
|
6584
|
+
if (decl.prop.toLowerCase() !== "font") return decl.value;
|
|
6585
|
+
const shorthand = parseFontShorthand(decl.value);
|
|
6586
|
+
return shorthand.certain ? shorthand.families : "";
|
|
6587
|
+
}
|
|
6588
|
+
var FONT_FAMILY_PROPS = /^font(?:-family)?$/i;
|
|
6512
6589
|
function extractUsedFontFamilies(styles) {
|
|
6513
6590
|
const used = [];
|
|
6514
6591
|
const seen = /* @__PURE__ */ new Set();
|
|
6515
6592
|
const properties = collectFontCustomProperties(styles);
|
|
6516
6593
|
for (const style of styles) {
|
|
6517
|
-
parseFontCss(style.content)?.walkDecls(
|
|
6518
|
-
|
|
6519
|
-
for (const name of resolveUsedFontNames(decl.value, properties)) {
|
|
6594
|
+
parseFontCss(style.content)?.walkDecls(FONT_FAMILY_PROPS, (decl) => {
|
|
6595
|
+
for (const name of resolveUsedFontNames(usedFamilyList(decl), properties)) {
|
|
6520
6596
|
if (!GENERIC_FAMILIES.has(name) && !seen.has(name)) {
|
|
6521
6597
|
seen.add(name);
|
|
6522
6598
|
used.push(name);
|
|
@@ -6618,9 +6694,33 @@ var fontRules = [
|
|
|
6618
6694
|
code: "font_family_without_font_face",
|
|
6619
6695
|
severity: "error",
|
|
6620
6696
|
message: `Font ${undeclared.length === 1 ? "family" : "families"} used without @font-face declaration: ${undeclared.join(", ")}. These are not in the auto-resolved font list, so the renderer cannot supply them automatically. Text will fall back to a generic font, producing incorrect typography in the video.`,
|
|
6621
|
-
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files
|
|
6697
|
+
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files, or switch to a Google Fonts family. An OS font such as PingFang SC or Hiragino Sans renders locally from the installed copy, but a cloud render has no copy and refuses it."
|
|
6622
6698
|
});
|
|
6623
6699
|
return findings;
|
|
6700
|
+
},
|
|
6701
|
+
({ styles, rawSource, options, locate }) => {
|
|
6702
|
+
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
6703
|
+
const findings = [];
|
|
6704
|
+
for (const style of styles) {
|
|
6705
|
+
parseFontCss(style.content)?.walkAtRules(/^font-face$/i, (rule) => {
|
|
6706
|
+
let family = null;
|
|
6707
|
+
let sources = "";
|
|
6708
|
+
rule.walkDecls((decl) => {
|
|
6709
|
+
const prop = decl.prop.toLowerCase();
|
|
6710
|
+
if (prop === "font-family") family = normalizeUsedFontName(decl.value);
|
|
6711
|
+
else if (prop === "src") sources += ` ${decl.value}`;
|
|
6712
|
+
});
|
|
6713
|
+
if (!family || !/\blocal\s*\(/i.test(sources) || /\burl\s*\(/i.test(sources)) return;
|
|
6714
|
+
findings.push({
|
|
6715
|
+
...locate(style, rule.source?.start?.offset),
|
|
6716
|
+
code: "font_face_local_only",
|
|
6717
|
+
severity: "warning",
|
|
6718
|
+
message: `@font-face '${family}' only names a font installed on this machine (src: local()). A local render uses the installed copy; a cloud render or another machine has none and falls back to a different font.`,
|
|
6719
|
+
fixHint: "Add a url() source to the font file (.woff2) inside the project, or use a Google Fonts family, so every render draws the same glyphs."
|
|
6720
|
+
});
|
|
6721
|
+
});
|
|
6722
|
+
}
|
|
6723
|
+
return findings;
|
|
6624
6724
|
}
|
|
6625
6725
|
];
|
|
6626
6726
|
function uniqueFontLocation(styles, families, locate) {
|
|
@@ -6628,10 +6728,9 @@ function uniqueFontLocation(styles, families, locate) {
|
|
|
6628
6728
|
const properties = collectFontCustomProperties(styles);
|
|
6629
6729
|
for (const style of styles) {
|
|
6630
6730
|
try {
|
|
6631
|
-
postcss3.parse(style.content).walkDecls(
|
|
6632
|
-
|
|
6633
|
-
if (!
|
|
6634
|
-
return;
|
|
6731
|
+
postcss3.parse(style.content).walkDecls(FONT_FAMILY_PROPS, (decl) => {
|
|
6732
|
+
const named = resolveUsedFontNames(usedFamilyList(decl), properties);
|
|
6733
|
+
if (!named.some((name) => families.includes(name))) return;
|
|
6635
6734
|
locations.push(locate(style, decl.source?.start?.offset));
|
|
6636
6735
|
});
|
|
6637
6736
|
} catch {
|
|
@@ -8134,6 +8233,7 @@ function isSnippetFragment(html) {
|
|
|
8134
8233
|
export {
|
|
8135
8234
|
LINT_RULE_COUNT,
|
|
8136
8235
|
LINT_RULE_GROUP_COUNTS,
|
|
8236
|
+
audioAttributeShapeError,
|
|
8137
8237
|
collectExternalStyles,
|
|
8138
8238
|
lintHyperframeHtml,
|
|
8139
8239
|
lintMediaUrls,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/lint",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/heygen-com/hyperframes",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"linkedom": "^0.18.12",
|
|
36
36
|
"postcss": "^8.5.8",
|
|
37
37
|
"postcss-selector-parser": "^7.1.4",
|
|
38
|
-
"@hyperframes/parsers": "0.9.
|
|
38
|
+
"@hyperframes/parsers": "0.9.14"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^25.0.10",
|