@brandlab-digital/conformance 3.8.0 → 3.10.0
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/current-release.js +5 -5
- package/dist/current-release.js.map +1 -1
- package/dist/engineering.js +4 -2
- package/dist/engineering.js.map +1 -1
- package/dist/site-system/checks.d.ts +1 -1
- package/dist/site-system/checks.js +1 -1
- package/dist/site-system/checks.js.map +1 -1
- package/dist/site-system/rule-modules.d.ts.map +1 -1
- package/dist/site-system/rule-modules.js +22 -1
- package/dist/site-system/rule-modules.js.map +1 -1
- package/dist/site-system/rules/form/c11-contact-route.d.ts +5 -0
- package/dist/site-system/rules/form/c11-contact-route.d.ts.map +1 -0
- package/dist/site-system/rules/form/c11-contact-route.js +132 -0
- package/dist/site-system/rules/form/c11-contact-route.js.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts +20 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.js +67 -0
- package/dist/site-system/rules/form/c7-thank-you.js.map +1 -0
- package/dist/site-system/rules/form/c8-forgiving-form.d.ts +19 -0
- package/dist/site-system/rules/form/c8-forgiving-form.d.ts.map +1 -0
- package/dist/site-system/rules/form/c8-forgiving-form.js +128 -0
- package/dist/site-system/rules/form/c8-forgiving-form.js.map +1 -0
- package/dist/site-system/rules/form/index.d.ts +14 -0
- package/dist/site-system/rules/form/index.d.ts.map +1 -0
- package/dist/site-system/rules/form/index.js +13 -0
- package/dist/site-system/rules/form/index.js.map +1 -0
- package/dist/site-system/rules/library/common.d.ts +36 -0
- package/dist/site-system/rules/library/common.d.ts.map +1 -0
- package/dist/site-system/rules/library/common.js +70 -0
- package/dist/site-system/rules/library/common.js.map +1 -0
- package/dist/site-system/rules/library/index.d.ts +16 -0
- package/dist/site-system/rules/library/index.d.ts.map +1 -0
- package/dist/site-system/rules/library/index.js +19 -0
- package/dist/site-system/rules/library/index.js.map +1 -0
- package/dist/site-system/rules/library/k1-shared-components.d.ts +29 -0
- package/dist/site-system/rules/library/k1-shared-components.d.ts.map +1 -0
- package/dist/site-system/rules/library/k1-shared-components.js +97 -0
- package/dist/site-system/rules/library/k1-shared-components.js.map +1 -0
- package/dist/site-system/rules/navigation/extract.d.ts +50 -0
- package/dist/site-system/rules/navigation/extract.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/extract.js +220 -0
- package/dist/site-system/rules/navigation/extract.js.map +1 -0
- package/dist/site-system/rules/navigation/html.d.ts +23 -0
- package/dist/site-system/rules/navigation/html.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/html.js +219 -0
- package/dist/site-system/rules/navigation/html.js.map +1 -0
- package/dist/site-system/rules/navigation/index.d.ts +37 -0
- package/dist/site-system/rules/navigation/index.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/index.js +163 -0
- package/dist/site-system/rules/navigation/index.js.map +1 -0
- package/dist/site-system/rules/navigation/record.d.ts +8 -0
- package/dist/site-system/rules/navigation/record.d.ts.map +1 -0
- package/dist/site-system/rules/navigation/record.js +39 -0
- package/dist/site-system/rules/navigation/record.js.map +1 -0
- package/dist/site-system/rules/sections/exif.d.ts +22 -0
- package/dist/site-system/rules/sections/exif.d.ts.map +1 -0
- package/dist/site-system/rules/sections/exif.js +218 -0
- package/dist/site-system/rules/sections/exif.js.map +1 -0
- package/dist/site-system/rules/sections/images.d.ts +31 -0
- package/dist/site-system/rules/sections/images.d.ts.map +1 -0
- package/dist/site-system/rules/sections/images.js +96 -0
- package/dist/site-system/rules/sections/images.js.map +1 -0
- package/dist/site-system/rules/sections/index.d.ts +20 -0
- package/dist/site-system/rules/sections/index.d.ts.map +1 -0
- package/dist/site-system/rules/sections/index.js +34 -0
- package/dist/site-system/rules/sections/index.js.map +1 -0
- package/dist/site-system/rules/sections/k11-density.d.ts +23 -0
- package/dist/site-system/rules/sections/k11-density.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k11-density.js +88 -0
- package/dist/site-system/rules/sections/k11-density.js.map +1 -0
- package/dist/site-system/rules/sections/k12-contact.d.ts +13 -0
- package/dist/site-system/rules/sections/k12-contact.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k12-contact.js +70 -0
- package/dist/site-system/rules/sections/k12-contact.js.map +1 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +11 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.js +126 -0
- package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -0
- package/dist/site-system/rules/sections/n9-footer.d.ts +40 -0
- package/dist/site-system/rules/sections/n9-footer.d.ts.map +1 -0
- package/dist/site-system/rules/sections/n9-footer.js +186 -0
- package/dist/site-system/rules/sections/n9-footer.js.map +1 -0
- package/dist/site-system/rules/sections/t6-media-entries.d.ts +14 -0
- package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t6-media-entries.js +96 -0
- package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -0
- package/dist/site-system/rules/sections/t7-main-images.d.ts +13 -0
- package/dist/site-system/rules/sections/t7-main-images.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t7-main-images.js +41 -0
- package/dist/site-system/rules/sections/t7-main-images.js.map +1 -0
- package/dist/site-system/rules/sections/t8-prices.d.ts +13 -0
- package/dist/site-system/rules/sections/t8-prices.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t8-prices.js +92 -0
- package/dist/site-system/rules/sections/t8-prices.js.map +1 -0
- package/dist/site-system/rules/sections/t9-location-metadata.d.ts +3 -0
- package/dist/site-system/rules/sections/t9-location-metadata.d.ts.map +1 -0
- package/dist/site-system/rules/sections/t9-location-metadata.js +54 -0
- package/dist/site-system/rules/sections/t9-location-metadata.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Romie recorded for the menu: the `navigation` of `defineSiteStrategy` in `src/data/strategy.ts` (an exact
|
|
3
|
+
* order, labels outside the closed list, whether Areas is a dropdown). It is read as plain data with the same literal
|
|
4
|
+
* reader the other strategy checks use, so the site never has to run.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
import { D4_STRATEGY_FILE } from "../design/d4-distinctness.js";
|
|
10
|
+
import { isFullyResolved, isLiteralObject, LiteralReader } from "../enquiry/literal.js";
|
|
11
|
+
function propertyKey(name) {
|
|
12
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function readRecordedNavigation(projectRoot) {
|
|
15
|
+
const file = join(projectRoot, D4_STRATEGY_FILE);
|
|
16
|
+
if (!existsSync(file))
|
|
17
|
+
return {};
|
|
18
|
+
const reader = new LiteralReader(readFileSync(file, "utf8"), D4_STRATEGY_FILE);
|
|
19
|
+
const argument = reader.callArguments("defineSiteStrategy")[0] ?? reader.constant("strategy");
|
|
20
|
+
// A strategy that is not a literal is reported by check D4; there is nothing for N1 to add.
|
|
21
|
+
if (!argument || !ts.isObjectLiteralExpression(argument))
|
|
22
|
+
return {};
|
|
23
|
+
for (const property of argument.properties) {
|
|
24
|
+
if (ts.isPropertyAssignment(property) && propertyKey(property.name) === "navigation") {
|
|
25
|
+
return readValue(reader.evaluate(property.initializer));
|
|
26
|
+
}
|
|
27
|
+
if (ts.isShorthandPropertyAssignment(property) && property.name.text === "navigation") {
|
|
28
|
+
return readValue(reader.evaluate(property.name));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
function readValue(value) {
|
|
34
|
+
if (!isLiteralObject(value) || !isFullyResolved(value)) {
|
|
35
|
+
return { problem: `${D4_STRATEGY_FILE} records a navigation that is not plain data, so N1 cannot read it; write it as a literal.` };
|
|
36
|
+
}
|
|
37
|
+
return { recorded: value };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../../../src/site-system/rules/navigation/record.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AASxF,SAAS,WAAW,CAAC,IAAqB;IACxC,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,WAAmB;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IACjD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAC/E,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC9F,4FAA4F;IAC5F,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC,yBAAyB,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACpE,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3C,IAAI,EAAE,CAAC,oBAAoB,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,YAAY,EAAE,CAAC;YACrF,OAAO,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,EAAE,CAAC,6BAA6B,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YACtF,OAAO,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,SAAS,CAAC,KAA4C;IAC7D,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,OAAO,EAAE,OAAO,EAAE,GAAG,gBAAgB,4FAA4F,EAAE,CAAC;IACtI,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,KAAgC,EAAE,CAAC;AACxD,CAAC","sourcesContent":["/**\n * What Romie recorded for the menu: the `navigation` of `defineSiteStrategy` in `src/data/strategy.ts` (an exact\n * order, labels outside the closed list, whether Areas is a dropdown). It is read as plain data with the same literal\n * reader the other strategy checks use, so the site never has to run.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport ts from \"typescript\";\nimport { D4_STRATEGY_FILE } from \"../design/d4-distinctness.js\";\nimport { isFullyResolved, isLiteralObject, LiteralReader } from \"../enquiry/literal.js\";\n\nexport interface RecordedNavigationRead {\n /** The recorded menu as plain data; absent when the strategy records none. */\n recorded?: Record<string, unknown>;\n /** Why a recorded menu could not be read. */\n problem?: string;\n}\n\nfunction propertyKey(name: ts.PropertyName): string | undefined {\n return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : undefined;\n}\n\nexport function readRecordedNavigation(projectRoot: string): RecordedNavigationRead {\n const file = join(projectRoot, D4_STRATEGY_FILE);\n if (!existsSync(file)) return {};\n const reader = new LiteralReader(readFileSync(file, \"utf8\"), D4_STRATEGY_FILE);\n const argument = reader.callArguments(\"defineSiteStrategy\")[0] ?? reader.constant(\"strategy\");\n // A strategy that is not a literal is reported by check D4; there is nothing for N1 to add.\n if (!argument || !ts.isObjectLiteralExpression(argument)) return {};\n for (const property of argument.properties) {\n if (ts.isPropertyAssignment(property) && propertyKey(property.name) === \"navigation\") {\n return readValue(reader.evaluate(property.initializer));\n }\n if (ts.isShorthandPropertyAssignment(property) && property.name.text === \"navigation\") {\n return readValue(reader.evaluate(property.name));\n }\n }\n return {};\n}\n\nfunction readValue(value: ReturnType<LiteralReader[\"evaluate\"]>): RecordedNavigationRead {\n if (!isLiteralObject(value) || !isFullyResolved(value)) {\n return { problem: `${D4_STRATEGY_FILE} records a navigation that is not plain data, so N1 cannot read it; write it as a literal.` };\n }\n return { recorded: value as Record<string, unknown> };\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads location metadata out of an image file without a dependency (T9). A photo straight off a phone carries the GPS
|
|
3
|
+
* position of the job site, and often the street and suburb; none of it may ship in `public/`.
|
|
4
|
+
*
|
|
5
|
+
* What counts as location metadata:
|
|
6
|
+
* - an EXIF GPS block with any tag beyond the version (latitude, longitude, altitude, a timestamp, a processing method);
|
|
7
|
+
* - XMP `exif:GPS*` properties and `photoshop:City`, `photoshop:State` and `Iptc4xmpCore:Location` (sublocation);
|
|
8
|
+
* - IPTC-IIM city (2:90), sublocation (2:92) and province or state (2:95) records.
|
|
9
|
+
*
|
|
10
|
+
* Formats: JPEG (APP1 EXIF and XMP, APP13 IPTC), PNG (`eXIf`, and `iTXt`/`tEXt` XMP), WebP (`EXIF` and `XMP ` chunks)
|
|
11
|
+
* and HEIC/HEIF/AVIF (the `Exif` item is found by its TIFF header, and XMP by its packet; this is a scan, not a full
|
|
12
|
+
* ISO box parser). A file in none of those formats, or one the reader cannot walk, reports nothing.
|
|
13
|
+
*/
|
|
14
|
+
export interface LocationMetadata {
|
|
15
|
+
/** Short names of what was found, for example "EXIF GPS (latitude, longitude)". */
|
|
16
|
+
readonly found: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/** Reads a TIFF block (EXIF) and returns the names of the GPS tags it carries. */
|
|
19
|
+
export declare function gpsTagsInTiff(bytes: Uint8Array, start: number): string[];
|
|
20
|
+
/** The location metadata an image file carries; an empty `found` for a clean file or a format this reader skips. */
|
|
21
|
+
export declare function locationMetadata(bytes: Uint8Array): LocationMetadata;
|
|
22
|
+
//# sourceMappingURL=exif.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exif.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/exif.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAiBD,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAuCxE;AA4HD,oHAAoH;AACpH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAUpE"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads location metadata out of an image file without a dependency (T9). A photo straight off a phone carries the GPS
|
|
3
|
+
* position of the job site, and often the street and suburb; none of it may ship in `public/`.
|
|
4
|
+
*
|
|
5
|
+
* What counts as location metadata:
|
|
6
|
+
* - an EXIF GPS block with any tag beyond the version (latitude, longitude, altitude, a timestamp, a processing method);
|
|
7
|
+
* - XMP `exif:GPS*` properties and `photoshop:City`, `photoshop:State` and `Iptc4xmpCore:Location` (sublocation);
|
|
8
|
+
* - IPTC-IIM city (2:90), sublocation (2:92) and province or state (2:95) records.
|
|
9
|
+
*
|
|
10
|
+
* Formats: JPEG (APP1 EXIF and XMP, APP13 IPTC), PNG (`eXIf`, and `iTXt`/`tEXt` XMP), WebP (`EXIF` and `XMP ` chunks)
|
|
11
|
+
* and HEIC/HEIF/AVIF (the `Exif` item is found by its TIFF header, and XMP by its packet; this is a scan, not a full
|
|
12
|
+
* ISO box parser). A file in none of those formats, or one the reader cannot walk, reports nothing.
|
|
13
|
+
*/
|
|
14
|
+
const GPS_IFD_POINTER = 0x8825;
|
|
15
|
+
const GPS_VERSION_TAG = 0x0000;
|
|
16
|
+
const GPS_TAG_NAMES = {
|
|
17
|
+
1: "latitude", 2: "latitude", 3: "longitude", 4: "longitude", 5: "altitude", 6: "altitude", 7: "time", 29: "date", 27: "processing method", 28: "area name",
|
|
18
|
+
};
|
|
19
|
+
const XMP_LOCATION = /<exif:GPS(?:Latitude|Longitude|Altitude|Position)\b|\bexif:GPS(?:Latitude|Longitude|Altitude)\s*=|<photoshop:(?:City|State)\b|\bphotoshop:(?:City|State)\s*=|<Iptc4xmpCore:Location\b|\bIptc4xmpCore:Location\s*=/;
|
|
20
|
+
const IPTC_LOCATION_RECORDS = { 90: "city", 92: "sublocation", 95: "state" };
|
|
21
|
+
function ascii(bytes, start, length) {
|
|
22
|
+
let text = "";
|
|
23
|
+
for (let index = start; index < Math.min(bytes.length, start + length); index += 1)
|
|
24
|
+
text += String.fromCharCode(bytes[index]);
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
/** Reads a TIFF block (EXIF) and returns the names of the GPS tags it carries. */
|
|
28
|
+
export function gpsTagsInTiff(bytes, start) {
|
|
29
|
+
const header = ascii(bytes, start, 2);
|
|
30
|
+
if (header !== "II" && header !== "MM")
|
|
31
|
+
return [];
|
|
32
|
+
const little = header === "II";
|
|
33
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
34
|
+
const u16 = (offset) => (offset + 2 <= bytes.length ? view.getUint16(offset, little) : 0);
|
|
35
|
+
const u32 = (offset) => (offset + 4 <= bytes.length ? view.getUint32(offset, little) : 0);
|
|
36
|
+
if (u16(start + 2) !== 42)
|
|
37
|
+
return [];
|
|
38
|
+
const entries = (ifdOffset) => {
|
|
39
|
+
const at = start + ifdOffset;
|
|
40
|
+
if (ifdOffset === 0 || at + 2 > bytes.length)
|
|
41
|
+
return [];
|
|
42
|
+
const count = Math.min(u16(at), 512);
|
|
43
|
+
const found = [];
|
|
44
|
+
for (let index = 0; index < count; index += 1) {
|
|
45
|
+
const entry = at + 2 + index * 12;
|
|
46
|
+
if (entry + 12 > bytes.length)
|
|
47
|
+
break;
|
|
48
|
+
found.push({ tag: u16(entry), valueOffset: entry + 8 });
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
};
|
|
52
|
+
const names = new Set();
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
let ifd = u32(start + 4);
|
|
55
|
+
for (let hop = 0; hop < 4 && ifd !== 0 && !seen.has(ifd); hop += 1) {
|
|
56
|
+
seen.add(ifd);
|
|
57
|
+
const list = entries(ifd);
|
|
58
|
+
const pointer = list.find((entry) => entry.tag === GPS_IFD_POINTER);
|
|
59
|
+
if (pointer) {
|
|
60
|
+
for (const gps of entries(u32(pointer.valueOffset))) {
|
|
61
|
+
if (gps.tag === GPS_VERSION_TAG)
|
|
62
|
+
continue;
|
|
63
|
+
names.add(GPS_TAG_NAMES[gps.tag] ?? "position data");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const next = start + ifd + 2 + Math.min(u16(start + ifd), 512) * 12;
|
|
67
|
+
ifd = next + 4 <= bytes.length ? u32(next) : 0;
|
|
68
|
+
}
|
|
69
|
+
return [...names];
|
|
70
|
+
}
|
|
71
|
+
function xmpFound(text) {
|
|
72
|
+
return XMP_LOCATION.test(text);
|
|
73
|
+
}
|
|
74
|
+
/** IPTC-IIM records inside a Photoshop APP13 resource block: the names of the location records with text. */
|
|
75
|
+
function iptcLocations(bytes, start, end) {
|
|
76
|
+
const names = new Set();
|
|
77
|
+
// The IIM stream sits in the "8BIM" resource 0x0404; scan from there for record 2 datasets (0x1C 0x02 <id> <len>).
|
|
78
|
+
let from = -1;
|
|
79
|
+
for (let index = start; index + 6 < end; index += 1) {
|
|
80
|
+
if (ascii(bytes, index, 4) === "8BIM" && bytes[index + 4] === 0x04 && bytes[index + 5] === 0x04) {
|
|
81
|
+
from = index + 6;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (from === -1)
|
|
86
|
+
return [];
|
|
87
|
+
for (let index = from; index + 5 < end; index += 1) {
|
|
88
|
+
if (bytes[index] !== 0x1c || bytes[index + 1] !== 2)
|
|
89
|
+
continue;
|
|
90
|
+
const dataset = bytes[index + 2];
|
|
91
|
+
const length = (bytes[index + 3] << 8) | bytes[index + 4];
|
|
92
|
+
const name = IPTC_LOCATION_RECORDS[dataset];
|
|
93
|
+
if (name && length > 0 && length < 256 && index + 5 + length <= end)
|
|
94
|
+
names.add(name);
|
|
95
|
+
}
|
|
96
|
+
return [...names];
|
|
97
|
+
}
|
|
98
|
+
function describe(tiffGps, xmp, iptc) {
|
|
99
|
+
const found = [];
|
|
100
|
+
if (tiffGps.length > 0)
|
|
101
|
+
found.push(`EXIF GPS (${[...new Set(tiffGps)].join(", ")})`);
|
|
102
|
+
if (xmp)
|
|
103
|
+
found.push("XMP location");
|
|
104
|
+
if (iptc.length > 0)
|
|
105
|
+
found.push(`IPTC ${iptc.join(", ")}`);
|
|
106
|
+
return found;
|
|
107
|
+
}
|
|
108
|
+
function jpeg(bytes) {
|
|
109
|
+
const tiff = [];
|
|
110
|
+
let xmp = false;
|
|
111
|
+
const iptc = [];
|
|
112
|
+
let offset = 2;
|
|
113
|
+
while (offset + 4 <= bytes.length && bytes[offset] === 0xff) {
|
|
114
|
+
const marker = bytes[offset + 1];
|
|
115
|
+
if (marker === 0xff) {
|
|
116
|
+
offset += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (marker === 0xda || marker === 0xd9)
|
|
120
|
+
break;
|
|
121
|
+
if (marker >= 0xd0 && marker <= 0xd7) {
|
|
122
|
+
offset += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const length = (bytes[offset + 2] << 8) | bytes[offset + 3];
|
|
126
|
+
const body = offset + 4;
|
|
127
|
+
const end = Math.min(bytes.length, offset + 2 + length);
|
|
128
|
+
if (marker === 0xe1) {
|
|
129
|
+
if (ascii(bytes, body, 6) === "Exif\0\0")
|
|
130
|
+
tiff.push(...gpsTagsInTiff(bytes, body + 6));
|
|
131
|
+
else if (ascii(bytes, body, 29).startsWith("http://ns.adobe.com/xap/1.0/"))
|
|
132
|
+
xmp ||= xmpFound(ascii(bytes, body, end - body));
|
|
133
|
+
}
|
|
134
|
+
else if (marker === 0xed && ascii(bytes, body, 14) === "Photoshop 3.0\0") {
|
|
135
|
+
iptc.push(...iptcLocations(bytes, body + 14, end));
|
|
136
|
+
}
|
|
137
|
+
offset = end;
|
|
138
|
+
if (length < 2)
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
return { found: describe(tiff, xmp, iptc) };
|
|
142
|
+
}
|
|
143
|
+
function png(bytes) {
|
|
144
|
+
const tiff = [];
|
|
145
|
+
let xmp = false;
|
|
146
|
+
let offset = 8;
|
|
147
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
148
|
+
while (offset + 12 <= bytes.length) {
|
|
149
|
+
const length = view.getUint32(offset);
|
|
150
|
+
const type = ascii(bytes, offset + 4, 4);
|
|
151
|
+
const body = offset + 8;
|
|
152
|
+
if (type === "eXIf")
|
|
153
|
+
tiff.push(...gpsTagsInTiff(bytes, body));
|
|
154
|
+
else if ((type === "iTXt" || type === "tEXt") && ascii(bytes, body, 17).startsWith("XML:com.adobe.xmp"))
|
|
155
|
+
xmp ||= xmpFound(ascii(bytes, body, Math.min(length, 2_000_000)));
|
|
156
|
+
if (type === "IEND")
|
|
157
|
+
break;
|
|
158
|
+
offset = body + length + 4;
|
|
159
|
+
}
|
|
160
|
+
return { found: describe(tiff, xmp, []) };
|
|
161
|
+
}
|
|
162
|
+
function webp(bytes) {
|
|
163
|
+
const tiff = [];
|
|
164
|
+
let xmp = false;
|
|
165
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
166
|
+
let offset = 12;
|
|
167
|
+
while (offset + 8 <= bytes.length) {
|
|
168
|
+
const type = ascii(bytes, offset, 4);
|
|
169
|
+
const length = view.getUint32(offset + 4, true);
|
|
170
|
+
const body = offset + 8;
|
|
171
|
+
if (type === "EXIF") {
|
|
172
|
+
const start = ascii(bytes, body, 6) === "Exif\0\0" ? body + 6 : body;
|
|
173
|
+
tiff.push(...gpsTagsInTiff(bytes, start));
|
|
174
|
+
}
|
|
175
|
+
else if (type === "XMP ") {
|
|
176
|
+
xmp ||= xmpFound(ascii(bytes, body, Math.min(length, 2_000_000)));
|
|
177
|
+
}
|
|
178
|
+
offset = body + length + (length % 2);
|
|
179
|
+
}
|
|
180
|
+
return { found: describe(tiff, xmp, []) };
|
|
181
|
+
}
|
|
182
|
+
/** HEIC, HEIF and AVIF keep EXIF in an item and XMP in another; find each by its signature. */
|
|
183
|
+
function isobmff(bytes) {
|
|
184
|
+
const tiff = [];
|
|
185
|
+
const limit = Math.min(bytes.length, 8_000_000);
|
|
186
|
+
for (let index = 0; index + 12 < limit; index += 1) {
|
|
187
|
+
if (bytes[index] === 0x45 && bytes[index + 1] === 0x78 && bytes[index + 2] === 0x69 && bytes[index + 3] === 0x66) {
|
|
188
|
+
// "Exif": a 4-byte offset to the TIFF header follows the item's tag, then the TIFF header "II*\0" or "MM\0*".
|
|
189
|
+
for (let start = index + 4; start < index + 16 && start + 4 < limit; start += 1) {
|
|
190
|
+
const marker = ascii(bytes, start, 2);
|
|
191
|
+
if ((marker === "II" && bytes[start + 2] === 42 && bytes[start + 3] === 0) || (marker === "MM" && bytes[start + 2] === 0 && bytes[start + 3] === 42)) {
|
|
192
|
+
tiff.push(...gpsTagsInTiff(bytes, start));
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const text = ascii(bytes, 0, limit);
|
|
199
|
+
return { found: describe(tiff, text.includes("<x:xmpmeta") && xmpFound(text), []) };
|
|
200
|
+
}
|
|
201
|
+
/** The location metadata an image file carries; an empty `found` for a clean file or a format this reader skips. */
|
|
202
|
+
export function locationMetadata(bytes) {
|
|
203
|
+
try {
|
|
204
|
+
if (bytes.length > 4 && bytes[0] === 0xff && bytes[1] === 0xd8)
|
|
205
|
+
return jpeg(bytes);
|
|
206
|
+
if (bytes.length > 8 && ascii(bytes, 1, 3) === "PNG")
|
|
207
|
+
return png(bytes);
|
|
208
|
+
if (bytes.length > 12 && ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WEBP")
|
|
209
|
+
return webp(bytes);
|
|
210
|
+
if (bytes.length > 12 && ascii(bytes, 4, 4) === "ftyp")
|
|
211
|
+
return isobmff(bytes);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// an unreadable file reports nothing
|
|
215
|
+
}
|
|
216
|
+
return { found: [] };
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=exif.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exif.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/exif.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads location metadata out of an image file without a dependency (T9). A photo straight off a phone carries the GPS\n * position of the job site, and often the street and suburb; none of it may ship in `public/`.\n *\n * What counts as location metadata:\n * - an EXIF GPS block with any tag beyond the version (latitude, longitude, altitude, a timestamp, a processing method);\n * - XMP `exif:GPS*` properties and `photoshop:City`, `photoshop:State` and `Iptc4xmpCore:Location` (sublocation);\n * - IPTC-IIM city (2:90), sublocation (2:92) and province or state (2:95) records.\n *\n * Formats: JPEG (APP1 EXIF and XMP, APP13 IPTC), PNG (`eXIf`, and `iTXt`/`tEXt` XMP), WebP (`EXIF` and `XMP ` chunks)\n * and HEIC/HEIF/AVIF (the `Exif` item is found by its TIFF header, and XMP by its packet; this is a scan, not a full\n * ISO box parser). A file in none of those formats, or one the reader cannot walk, reports nothing.\n */\n\nexport interface LocationMetadata {\n /** Short names of what was found, for example \"EXIF GPS (latitude, longitude)\". */\n readonly found: readonly string[];\n}\n\nconst GPS_IFD_POINTER = 0x8825;\nconst GPS_VERSION_TAG = 0x0000;\nconst GPS_TAG_NAMES: Readonly<Record<number, string>> = {\n 1: \"latitude\", 2: \"latitude\", 3: \"longitude\", 4: \"longitude\", 5: \"altitude\", 6: \"altitude\", 7: \"time\", 29: \"date\", 27: \"processing method\", 28: \"area name\",\n};\n\nconst XMP_LOCATION = /<exif:GPS(?:Latitude|Longitude|Altitude|Position)\\b|\\bexif:GPS(?:Latitude|Longitude|Altitude)\\s*=|<photoshop:(?:City|State)\\b|\\bphotoshop:(?:City|State)\\s*=|<Iptc4xmpCore:Location\\b|\\bIptc4xmpCore:Location\\s*=/;\nconst IPTC_LOCATION_RECORDS: Readonly<Record<number, string>> = { 90: \"city\", 92: \"sublocation\", 95: \"state\" };\n\nfunction ascii(bytes: Uint8Array, start: number, length: number): string {\n let text = \"\";\n for (let index = start; index < Math.min(bytes.length, start + length); index += 1) text += String.fromCharCode(bytes[index]!);\n return text;\n}\n\n/** Reads a TIFF block (EXIF) and returns the names of the GPS tags it carries. */\nexport function gpsTagsInTiff(bytes: Uint8Array, start: number): string[] {\n const header = ascii(bytes, start, 2);\n if (header !== \"II\" && header !== \"MM\") return [];\n const little = header === \"II\";\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const u16 = (offset: number) => (offset + 2 <= bytes.length ? view.getUint16(offset, little) : 0);\n const u32 = (offset: number) => (offset + 4 <= bytes.length ? view.getUint32(offset, little) : 0);\n if (u16(start + 2) !== 42) return [];\n\n const entries = (ifdOffset: number): { tag: number; valueOffset: number }[] => {\n const at = start + ifdOffset;\n if (ifdOffset === 0 || at + 2 > bytes.length) return [];\n const count = Math.min(u16(at), 512);\n const found: { tag: number; valueOffset: number }[] = [];\n for (let index = 0; index < count; index += 1) {\n const entry = at + 2 + index * 12;\n if (entry + 12 > bytes.length) break;\n found.push({ tag: u16(entry), valueOffset: entry + 8 });\n }\n return found;\n };\n\n const names = new Set<string>();\n const seen = new Set<number>();\n let ifd = u32(start + 4);\n for (let hop = 0; hop < 4 && ifd !== 0 && !seen.has(ifd); hop += 1) {\n seen.add(ifd);\n const list = entries(ifd);\n const pointer = list.find((entry) => entry.tag === GPS_IFD_POINTER);\n if (pointer) {\n for (const gps of entries(u32(pointer.valueOffset))) {\n if (gps.tag === GPS_VERSION_TAG) continue;\n names.add(GPS_TAG_NAMES[gps.tag] ?? \"position data\");\n }\n }\n const next = start + ifd + 2 + Math.min(u16(start + ifd), 512) * 12;\n ifd = next + 4 <= bytes.length ? u32(next) : 0;\n }\n return [...names];\n}\n\nfunction xmpFound(text: string): boolean {\n return XMP_LOCATION.test(text);\n}\n\n/** IPTC-IIM records inside a Photoshop APP13 resource block: the names of the location records with text. */\nfunction iptcLocations(bytes: Uint8Array, start: number, end: number): string[] {\n const names = new Set<string>();\n // The IIM stream sits in the \"8BIM\" resource 0x0404; scan from there for record 2 datasets (0x1C 0x02 <id> <len>).\n let from = -1;\n for (let index = start; index + 6 < end; index += 1) {\n if (ascii(bytes, index, 4) === \"8BIM\" && bytes[index + 4] === 0x04 && bytes[index + 5] === 0x04) {\n from = index + 6;\n break;\n }\n }\n if (from === -1) return [];\n for (let index = from; index + 5 < end; index += 1) {\n if (bytes[index] !== 0x1c || bytes[index + 1] !== 2) continue;\n const dataset = bytes[index + 2]!;\n const length = (bytes[index + 3]! << 8) | bytes[index + 4]!;\n const name = IPTC_LOCATION_RECORDS[dataset];\n if (name && length > 0 && length < 256 && index + 5 + length <= end) names.add(name);\n }\n return [...names];\n}\n\nfunction describe(tiffGps: readonly string[], xmp: boolean, iptc: readonly string[]): string[] {\n const found: string[] = [];\n if (tiffGps.length > 0) found.push(`EXIF GPS (${[...new Set(tiffGps)].join(\", \")})`);\n if (xmp) found.push(\"XMP location\");\n if (iptc.length > 0) found.push(`IPTC ${iptc.join(\", \")}`);\n return found;\n}\n\nfunction jpeg(bytes: Uint8Array): LocationMetadata {\n const tiff: string[] = [];\n let xmp = false;\n const iptc: string[] = [];\n let offset = 2;\n while (offset + 4 <= bytes.length && bytes[offset] === 0xff) {\n const marker = bytes[offset + 1]!;\n if (marker === 0xff) {\n offset += 1;\n continue;\n }\n if (marker === 0xda || marker === 0xd9) break;\n if (marker >= 0xd0 && marker <= 0xd7) {\n offset += 2;\n continue;\n }\n const length = (bytes[offset + 2]! << 8) | bytes[offset + 3]!;\n const body = offset + 4;\n const end = Math.min(bytes.length, offset + 2 + length);\n if (marker === 0xe1) {\n if (ascii(bytes, body, 6) === \"Exif\\0\\0\") tiff.push(...gpsTagsInTiff(bytes, body + 6));\n else if (ascii(bytes, body, 29).startsWith(\"http://ns.adobe.com/xap/1.0/\")) xmp ||= xmpFound(ascii(bytes, body, end - body));\n } else if (marker === 0xed && ascii(bytes, body, 14) === \"Photoshop 3.0\\0\") {\n iptc.push(...iptcLocations(bytes, body + 14, end));\n }\n offset = end;\n if (length < 2) break;\n }\n return { found: describe(tiff, xmp, iptc) };\n}\n\nfunction png(bytes: Uint8Array): LocationMetadata {\n const tiff: string[] = [];\n let xmp = false;\n let offset = 8;\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n while (offset + 12 <= bytes.length) {\n const length = view.getUint32(offset);\n const type = ascii(bytes, offset + 4, 4);\n const body = offset + 8;\n if (type === \"eXIf\") tiff.push(...gpsTagsInTiff(bytes, body));\n else if ((type === \"iTXt\" || type === \"tEXt\") && ascii(bytes, body, 17).startsWith(\"XML:com.adobe.xmp\")) xmp ||= xmpFound(ascii(bytes, body, Math.min(length, 2_000_000)));\n if (type === \"IEND\") break;\n offset = body + length + 4;\n }\n return { found: describe(tiff, xmp, []) };\n}\n\nfunction webp(bytes: Uint8Array): LocationMetadata {\n const tiff: string[] = [];\n let xmp = false;\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n let offset = 12;\n while (offset + 8 <= bytes.length) {\n const type = ascii(bytes, offset, 4);\n const length = view.getUint32(offset + 4, true);\n const body = offset + 8;\n if (type === \"EXIF\") {\n const start = ascii(bytes, body, 6) === \"Exif\\0\\0\" ? body + 6 : body;\n tiff.push(...gpsTagsInTiff(bytes, start));\n } else if (type === \"XMP \") {\n xmp ||= xmpFound(ascii(bytes, body, Math.min(length, 2_000_000)));\n }\n offset = body + length + (length % 2);\n }\n return { found: describe(tiff, xmp, []) };\n}\n\n/** HEIC, HEIF and AVIF keep EXIF in an item and XMP in another; find each by its signature. */\nfunction isobmff(bytes: Uint8Array): LocationMetadata {\n const tiff: string[] = [];\n const limit = Math.min(bytes.length, 8_000_000);\n for (let index = 0; index + 12 < limit; index += 1) {\n if (bytes[index] === 0x45 && bytes[index + 1] === 0x78 && bytes[index + 2] === 0x69 && bytes[index + 3] === 0x66) {\n // \"Exif\": a 4-byte offset to the TIFF header follows the item's tag, then the TIFF header \"II*\\0\" or \"MM\\0*\".\n for (let start = index + 4; start < index + 16 && start + 4 < limit; start += 1) {\n const marker = ascii(bytes, start, 2);\n if ((marker === \"II\" && bytes[start + 2] === 42 && bytes[start + 3] === 0) || (marker === \"MM\" && bytes[start + 2] === 0 && bytes[start + 3] === 42)) {\n tiff.push(...gpsTagsInTiff(bytes, start));\n break;\n }\n }\n }\n }\n const text = ascii(bytes, 0, limit);\n return { found: describe(tiff, text.includes(\"<x:xmpmeta\") && xmpFound(text), []) };\n}\n\n/** The location metadata an image file carries; an empty `found` for a clean file or a format this reader skips. */\nexport function locationMetadata(bytes: Uint8Array): LocationMetadata {\n try {\n if (bytes.length > 4 && bytes[0] === 0xff && bytes[1] === 0xd8) return jpeg(bytes);\n if (bytes.length > 8 && ascii(bytes, 1, 3) === \"PNG\") return png(bytes);\n if (bytes.length > 12 && ascii(bytes, 0, 4) === \"RIFF\" && ascii(bytes, 8, 4) === \"WEBP\") return webp(bytes);\n if (bytes.length > 12 && ascii(bytes, 4, 4) === \"ftyp\") return isobmff(bytes);\n } catch {\n // an unreadable file reports nothing\n }\n return { found: [] };\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type HtmlElement } from "../enquiry/html.js";
|
|
2
|
+
export declare const IMAGE_EXTENSIONS: readonly [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".heic", ".heif", ".svg"];
|
|
3
|
+
/** Files that are the business's mark or the browser's icon, not a photograph: they need no provenance. */
|
|
4
|
+
export declare const MEDIA_EXEMPT_NAME: RegExp;
|
|
5
|
+
/** The file behind an `<img>` source, or null for an inline `data:` image or an empty source. */
|
|
6
|
+
export declare function imageFile(source: string): string | null;
|
|
7
|
+
/** An image the page shows, with the element it came from. */
|
|
8
|
+
export interface PageImage {
|
|
9
|
+
readonly file: string;
|
|
10
|
+
readonly element: HtmlElement;
|
|
11
|
+
}
|
|
12
|
+
export declare function isAvatar(element: HtmlElement): boolean;
|
|
13
|
+
/** The image an `<img>` element shows: its `src`, or the first `srcset` candidate. Null for any other element. */
|
|
14
|
+
export declare function imageOf(element: HtmlElement): PageImage | null;
|
|
15
|
+
/** The images inside `scope`, in document order. */
|
|
16
|
+
export declare function imagesIn(scope: HtmlElement): PageImage[];
|
|
17
|
+
/** Whether the image needs a media.ts entry: not a logo, icon or SVG, and not a third-party profile photo. */
|
|
18
|
+
export declare function needsMediaEntry(image: PageImage): boolean;
|
|
19
|
+
export interface PublicImage {
|
|
20
|
+
/** The URL path the file is served at: `/images/jobs/a.jpg`. */
|
|
21
|
+
readonly web: string;
|
|
22
|
+
readonly file: string;
|
|
23
|
+
}
|
|
24
|
+
/** Every image file under the project's `public/` folder, and how many directories could not be read. */
|
|
25
|
+
export declare function scanPublicImages(projectRoot: string): {
|
|
26
|
+
images: PublicImage[];
|
|
27
|
+
unreadableDirectories: number;
|
|
28
|
+
};
|
|
29
|
+
/** Every image file under the project's `public/` folder (none when it has no such folder). */
|
|
30
|
+
export declare function publicImages(projectRoot: string): PublicImage[];
|
|
31
|
+
//# sourceMappingURL=images.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/images.ts"],"names":[],"mappings":"AAMA,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEzE,eAAO,MAAM,gBAAgB,wFAAyF,CAAC;AAEvH,2GAA2G;AAC3G,eAAO,MAAM,iBAAiB,QAAqG,CAAC;AAEpI,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBvD;AAED,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAMD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAKtD;AAED,kHAAkH;AAClH,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAK9D;AAED,oDAAoD;AACpD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,EAAE,CAExD;AAED,8GAA8G;AAC9G,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAGzD;AAED,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAAC,qBAAqB,EAAE,MAAM,CAAA;CAAE,CAwB9G;AAED,+FAA+F;AAC/F,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,EAAE,CAE/D"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the media rules (T6 and T7) read an image from the rendered HTML: the file it comes from, whatever `next/image`
|
|
3
|
+
* did to the URL. `next/image` writes `/_next/image?url=%2Fimages%2Fa.jpg&w=640&q=75`; the file is the `url` value.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
6
|
+
import { extname, join } from "node:path";
|
|
7
|
+
import { attr, descendants } from "../enquiry/html.js";
|
|
8
|
+
export const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".heic", ".heif", ".svg"];
|
|
9
|
+
/** Files that are the business's mark or the browser's icon, not a photograph: they need no provenance. */
|
|
10
|
+
export const MEDIA_EXEMPT_NAME = /(?:^|[\/._-])(?:logo|favicon|icon|apple-touch-icon|android-chrome|mstile|sprite)(?:[\/._\d-]|$)/i;
|
|
11
|
+
/** The file behind an `<img>` source, or null for an inline `data:` image or an empty source. */
|
|
12
|
+
export function imageFile(source) {
|
|
13
|
+
let value = source.trim();
|
|
14
|
+
if (!value || value.startsWith("data:") || value.startsWith("blob:"))
|
|
15
|
+
return null;
|
|
16
|
+
if (/^(?:https?:)?\/\/[^/]+\/_next\/image\?/i.test(value) || value.startsWith("/_next/image?")) {
|
|
17
|
+
const query = value.slice(value.indexOf("?") + 1);
|
|
18
|
+
const inner = new URLSearchParams(query).get("url");
|
|
19
|
+
if (!inner)
|
|
20
|
+
return null;
|
|
21
|
+
value = inner;
|
|
22
|
+
}
|
|
23
|
+
const cut = value.search(/[?#]/);
|
|
24
|
+
if (cut !== -1)
|
|
25
|
+
value = value.slice(0, cut);
|
|
26
|
+
try {
|
|
27
|
+
value = decodeURI(value);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// keep the source as written
|
|
31
|
+
}
|
|
32
|
+
return value || null;
|
|
33
|
+
}
|
|
34
|
+
function firstSrcsetUrl(srcset) {
|
|
35
|
+
return srcset?.split(",")[0]?.trim().split(/\s+/)[0];
|
|
36
|
+
}
|
|
37
|
+
export function isAvatar(element) {
|
|
38
|
+
for (let current = element; current && current.tag !== "#root"; current = current.parent) {
|
|
39
|
+
if (attr(current, "data-slot") === "avatar")
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/** The image an `<img>` element shows: its `src`, or the first `srcset` candidate. Null for any other element. */
|
|
45
|
+
export function imageOf(element) {
|
|
46
|
+
if (element.tag !== "img")
|
|
47
|
+
return null;
|
|
48
|
+
const source = attr(element, "src") ?? firstSrcsetUrl(attr(element, "srcset"));
|
|
49
|
+
const file = source ? imageFile(source) : null;
|
|
50
|
+
return file ? { file, element } : null;
|
|
51
|
+
}
|
|
52
|
+
/** The images inside `scope`, in document order. */
|
|
53
|
+
export function imagesIn(scope) {
|
|
54
|
+
return descendants(scope).flatMap((element) => imageOf(element) ?? []);
|
|
55
|
+
}
|
|
56
|
+
/** Whether the image needs a media.ts entry: not a logo, icon or SVG, and not a third-party profile photo. */
|
|
57
|
+
export function needsMediaEntry(image) {
|
|
58
|
+
if (/\.svg$/i.test(image.file) || MEDIA_EXEMPT_NAME.test(image.file) || isAvatar(image.element))
|
|
59
|
+
return false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** Every image file under the project's `public/` folder, and how many directories could not be read. */
|
|
63
|
+
export function scanPublicImages(projectRoot) {
|
|
64
|
+
const root = join(projectRoot, "public");
|
|
65
|
+
const images = [];
|
|
66
|
+
let unreadableDirectories = 0;
|
|
67
|
+
if (!existsSync(root))
|
|
68
|
+
return { images, unreadableDirectories };
|
|
69
|
+
const visit = (directory, web) => {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
unreadableDirectories += 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
79
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
80
|
+
continue;
|
|
81
|
+
const path = join(directory, entry.name);
|
|
82
|
+
if (entry.isDirectory())
|
|
83
|
+
visit(path, `${web}/${entry.name}`);
|
|
84
|
+
else if (entry.isFile() && IMAGE_EXTENSIONS.includes(extname(entry.name).toLowerCase())) {
|
|
85
|
+
images.push({ web: `${web}/${entry.name}`, file: path });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
visit(root, "");
|
|
90
|
+
return { images, unreadableDirectories };
|
|
91
|
+
}
|
|
92
|
+
/** Every image file under the project's `public/` folder (none when it has no such folder). */
|
|
93
|
+
export function publicImages(projectRoot) {
|
|
94
|
+
return scanPublicImages(projectRoot).images;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=images.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/images.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How the media rules (T6 and T7) read an image from the rendered HTML: the file it comes from, whatever `next/image`\n * did to the URL. `next/image` writes `/_next/image?url=%2Fimages%2Fa.jpg&w=640&q=75`; the file is the `url` value.\n */\nimport { existsSync, readdirSync, type Dirent } from \"node:fs\";\nimport { extname, join } from \"node:path\";\nimport { attr, descendants, type HtmlElement } from \"../enquiry/html.js\";\n\nexport const IMAGE_EXTENSIONS = [\".jpg\", \".jpeg\", \".png\", \".webp\", \".avif\", \".gif\", \".heic\", \".heif\", \".svg\"] as const;\n\n/** Files that are the business's mark or the browser's icon, not a photograph: they need no provenance. */\nexport const MEDIA_EXEMPT_NAME = /(?:^|[\\/._-])(?:logo|favicon|icon|apple-touch-icon|android-chrome|mstile|sprite)(?:[\\/._\\d-]|$)/i;\n\n/** The file behind an `<img>` source, or null for an inline `data:` image or an empty source. */\nexport function imageFile(source: string): string | null {\n let value = source.trim();\n if (!value || value.startsWith(\"data:\") || value.startsWith(\"blob:\")) return null;\n if (/^(?:https?:)?\\/\\/[^/]+\\/_next\\/image\\?/i.test(value) || value.startsWith(\"/_next/image?\")) {\n const query = value.slice(value.indexOf(\"?\") + 1);\n const inner = new URLSearchParams(query).get(\"url\");\n if (!inner) return null;\n value = inner;\n }\n const cut = value.search(/[?#]/);\n if (cut !== -1) value = value.slice(0, cut);\n try {\n value = decodeURI(value);\n } catch {\n // keep the source as written\n }\n return value || null;\n}\n\n/** An image the page shows, with the element it came from. */\nexport interface PageImage {\n readonly file: string;\n readonly element: HtmlElement;\n}\n\nfunction firstSrcsetUrl(srcset: string | undefined): string | undefined {\n return srcset?.split(\",\")[0]?.trim().split(/\\s+/)[0];\n}\n\nexport function isAvatar(element: HtmlElement): boolean {\n for (let current: HtmlElement | null = element; current && current.tag !== \"#root\"; current = current.parent) {\n if (attr(current, \"data-slot\") === \"avatar\") return true;\n }\n return false;\n}\n\n/** The image an `<img>` element shows: its `src`, or the first `srcset` candidate. Null for any other element. */\nexport function imageOf(element: HtmlElement): PageImage | null {\n if (element.tag !== \"img\") return null;\n const source = attr(element, \"src\") ?? firstSrcsetUrl(attr(element, \"srcset\"));\n const file = source ? imageFile(source) : null;\n return file ? { file, element } : null;\n}\n\n/** The images inside `scope`, in document order. */\nexport function imagesIn(scope: HtmlElement): PageImage[] {\n return descendants(scope).flatMap((element) => imageOf(element) ?? []);\n}\n\n/** Whether the image needs a media.ts entry: not a logo, icon or SVG, and not a third-party profile photo. */\nexport function needsMediaEntry(image: PageImage): boolean {\n if (/\\.svg$/i.test(image.file) || MEDIA_EXEMPT_NAME.test(image.file) || isAvatar(image.element)) return false;\n return true;\n}\n\nexport interface PublicImage {\n /** The URL path the file is served at: `/images/jobs/a.jpg`. */\n readonly web: string;\n readonly file: string;\n}\n\n/** Every image file under the project's `public/` folder, and how many directories could not be read. */\nexport function scanPublicImages(projectRoot: string): { images: PublicImage[]; unreadableDirectories: number } {\n const root = join(projectRoot, \"public\");\n const images: PublicImage[] = [];\n let unreadableDirectories = 0;\n if (!existsSync(root)) return { images, unreadableDirectories };\n const visit = (directory: string, web: string) => {\n let entries: Dirent[];\n try {\n entries = readdirSync(directory, { withFileTypes: true });\n } catch {\n unreadableDirectories += 1;\n return;\n }\n for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\") continue;\n const path = join(directory, entry.name);\n if (entry.isDirectory()) visit(path, `${web}/${entry.name}`);\n else if (entry.isFile() && (IMAGE_EXTENSIONS as readonly string[]).includes(extname(entry.name).toLowerCase())) {\n images.push({ web: `${web}/${entry.name}`, file: path });\n }\n }\n };\n visit(root, \"\");\n return { images, unreadableDirectories };\n}\n\n/** Every image file under the project's `public/` folder (none when it has no such folder). */\nexport function publicImages(projectRoot: string): PublicImage[] {\n return scanPublicImages(projectRoot).images;\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sections rule module (release 3.11.0): N9 footer content, K2 skin coverage, K11 density, K12-contact, T6 media
|
|
3
|
+
* entries, T7 main images, T8 prices and T9 location metadata. The rendered rules read the sampled pages' server HTML;
|
|
4
|
+
* T6 and T9 also read the project's `src/data/media.ts` and `public/` images.
|
|
5
|
+
*/
|
|
6
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
7
|
+
import { type EnquiryRuleInput } from "../enquiry/common.js";
|
|
8
|
+
export { footerColumns, footerOf, footerProblems, checkN9 } from "./n9-footer.js";
|
|
9
|
+
export { K2_REFERENCE_CLASS, K2_SKINNED_HOOKS, K2_SKINNED_SECTIONS, checkK2, hooksStyledBySite } from "./k2-skin-coverage.js";
|
|
10
|
+
export { K11_MAX_CALLS_TO_ACTION, K11_MAX_CARDS, callsToActionIn, cardsIn, checkK11 } from "./k11-density.js";
|
|
11
|
+
export { K12_CONTACT_ORDER, checkK12Contact } from "./k12-contact.js";
|
|
12
|
+
export { T6_MEDIA_PATH, T6_PROVENANCES, checkT6, readMediaManifest } from "./t6-media-entries.js";
|
|
13
|
+
export { checkT7, mainImageOf } from "./t7-main-images.js";
|
|
14
|
+
export { checkT8 } from "./t8-prices.js";
|
|
15
|
+
export { checkT9 } from "./t9-location-metadata.js";
|
|
16
|
+
export { gpsTagsInTiff, locationMetadata } from "./exif.js";
|
|
17
|
+
export { imageFile, imagesIn, publicImages } from "./images.js";
|
|
18
|
+
/** Runs N9, K2, K11, K12-contact, T6, T7, T8 and T9. */
|
|
19
|
+
export declare function runSectionRules(input: EnquiryRuleInput): Promise<readonly SiteSystemFinding[]>;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAUzE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAClF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9H,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC9G,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhE,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAYpG"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parsePages } from "../enquiry/common.js";
|
|
2
|
+
import { checkK11 } from "./k11-density.js";
|
|
3
|
+
import { checkK12Contact } from "./k12-contact.js";
|
|
4
|
+
import { checkK2 } from "./k2-skin-coverage.js";
|
|
5
|
+
import { checkN9 } from "./n9-footer.js";
|
|
6
|
+
import { checkT6 } from "./t6-media-entries.js";
|
|
7
|
+
import { checkT7 } from "./t7-main-images.js";
|
|
8
|
+
import { checkT8 } from "./t8-prices.js";
|
|
9
|
+
import { checkT9 } from "./t9-location-metadata.js";
|
|
10
|
+
export { footerColumns, footerOf, footerProblems, checkN9 } from "./n9-footer.js";
|
|
11
|
+
export { K2_REFERENCE_CLASS, K2_SKINNED_HOOKS, K2_SKINNED_SECTIONS, checkK2, hooksStyledBySite } from "./k2-skin-coverage.js";
|
|
12
|
+
export { K11_MAX_CALLS_TO_ACTION, K11_MAX_CARDS, callsToActionIn, cardsIn, checkK11 } from "./k11-density.js";
|
|
13
|
+
export { K12_CONTACT_ORDER, checkK12Contact } from "./k12-contact.js";
|
|
14
|
+
export { T6_MEDIA_PATH, T6_PROVENANCES, checkT6, readMediaManifest } from "./t6-media-entries.js";
|
|
15
|
+
export { checkT7, mainImageOf } from "./t7-main-images.js";
|
|
16
|
+
export { checkT8 } from "./t8-prices.js";
|
|
17
|
+
export { checkT9 } from "./t9-location-metadata.js";
|
|
18
|
+
export { gpsTagsInTiff, locationMetadata } from "./exif.js";
|
|
19
|
+
export { imageFile, imagesIn, publicImages } from "./images.js";
|
|
20
|
+
/** Runs N9, K2, K11, K12-contact, T6, T7, T8 and T9. */
|
|
21
|
+
export async function runSectionRules(input) {
|
|
22
|
+
const pages = parsePages(input.pages);
|
|
23
|
+
return [
|
|
24
|
+
...checkN9(pages, input.projectRoot),
|
|
25
|
+
...checkK2(pages, input.projectRoot),
|
|
26
|
+
...checkK11(pages),
|
|
27
|
+
...checkK12Contact(pages),
|
|
28
|
+
...checkT6(pages, input.projectRoot),
|
|
29
|
+
...checkT7(pages),
|
|
30
|
+
...checkT8(pages),
|
|
31
|
+
...checkT9(input.projectRoot),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAyB,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAClF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9H,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC9G,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhE,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAuB;IAC3D,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,OAAO;QACL,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC;QACpC,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC;QACpC,GAAG,QAAQ,CAAC,KAAK,CAAC;QAClB,GAAG,eAAe,CAAC,KAAK,CAAC;QACzB,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC;QACpC,GAAG,OAAO,CAAC,KAAK,CAAC;QACjB,GAAG,OAAO,CAAC,KAAK,CAAC;QACjB,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;KAC9B,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The sections rule module (release 3.11.0): N9 footer content, K2 skin coverage, K11 density, K12-contact, T6 media\n * entries, T7 main images, T8 prices and T9 location metadata. The rendered rules read the sampled pages' server HTML;\n * T6 and T9 also read the project's `src/data/media.ts` and `public/` images.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { parsePages, type EnquiryRuleInput } from \"../enquiry/common.js\";\nimport { checkK11 } from \"./k11-density.js\";\nimport { checkK12Contact } from \"./k12-contact.js\";\nimport { checkK2 } from \"./k2-skin-coverage.js\";\nimport { checkN9 } from \"./n9-footer.js\";\nimport { checkT6 } from \"./t6-media-entries.js\";\nimport { checkT7 } from \"./t7-main-images.js\";\nimport { checkT8 } from \"./t8-prices.js\";\nimport { checkT9 } from \"./t9-location-metadata.js\";\n\nexport { footerColumns, footerOf, footerProblems, checkN9 } from \"./n9-footer.js\";\nexport { K2_REFERENCE_CLASS, K2_SKINNED_HOOKS, K2_SKINNED_SECTIONS, checkK2, hooksStyledBySite } from \"./k2-skin-coverage.js\";\nexport { K11_MAX_CALLS_TO_ACTION, K11_MAX_CARDS, callsToActionIn, cardsIn, checkK11 } from \"./k11-density.js\";\nexport { K12_CONTACT_ORDER, checkK12Contact } from \"./k12-contact.js\";\nexport { T6_MEDIA_PATH, T6_PROVENANCES, checkT6, readMediaManifest } from \"./t6-media-entries.js\";\nexport { checkT7, mainImageOf } from \"./t7-main-images.js\";\nexport { checkT8 } from \"./t8-prices.js\";\nexport { checkT9 } from \"./t9-location-metadata.js\";\nexport { gpsTagsInTiff, locationMetadata } from \"./exif.js\";\nexport { imageFile, imagesIn, publicImages } from \"./images.js\";\n\n/** Runs N9, K2, K11, K12-contact, T6, T7, T8 and T9. */\nexport async function runSectionRules(input: EnquiryRuleInput): Promise<readonly SiteSystemFinding[]> {\n const pages = parsePages(input.pages);\n return [\n ...checkN9(pages, input.projectRoot),\n ...checkK2(pages, input.projectRoot),\n ...checkK11(pages),\n ...checkK12Contact(pages),\n ...checkT6(pages, input.projectRoot),\n ...checkT7(pages),\n ...checkT8(pages),\n ...checkT9(input.projectRoot),\n ];\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K11: section density (Site System v2 §13.2). A section does one thing: one H2, at most 8 cards and at most 2 calls to
|
|
3
|
+
* action. The rule measures what the server HTML shows. The spec also asks for body text no wider than about 75
|
|
4
|
+
* characters a line and section padding of at least the token value; those are CSS, which the HTML cannot show, so
|
|
5
|
+
* they stay with the contrast and spacing journeys and this rule says so rather than guessing.
|
|
6
|
+
*
|
|
7
|
+
* A card is one repeated unit with its own heading, image or quotation: an element with a card slot (`card`, `review`,
|
|
8
|
+
* `project`, `service`, `tile`, `post`, `teaser`), an `<article>`, a card class, or a list item that holds a heading, an
|
|
9
|
+
* image, a `<blockquote>` or a link with a sentence of its own. A call to action is a quote button, a call button, or the
|
|
10
|
+
* submit button of a form; an inline text phone link is a contact detail, not a call to action. A hero that holds the
|
|
11
|
+
* form also carries a quote button that scrolls to it (so the quote action is in the first screen on a phone); that
|
|
12
|
+
* button and the form's submit button are one action.
|
|
13
|
+
*/
|
|
14
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
15
|
+
import { type ParsedPage } from "../enquiry/common.js";
|
|
16
|
+
import { type HtmlElement } from "../enquiry/html.js";
|
|
17
|
+
export declare const K11_MAX_CARDS = 8;
|
|
18
|
+
export declare const K11_MAX_CALLS_TO_ACTION = 2;
|
|
19
|
+
/** The cards in a section: the outermost card-like elements, each counted once. */
|
|
20
|
+
export declare function cardsIn(section: HtmlElement): HtmlElement[];
|
|
21
|
+
export declare function callsToActionIn(section: HtmlElement): HtmlElement[];
|
|
22
|
+
export declare function checkK11(pages: readonly ParsedPage[]): SiteSystemFinding[];
|
|
23
|
+
//# sourceMappingURL=k11-density.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"k11-density.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/k11-density.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAA4D,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACjH,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGjF,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAmBzC,mFAAmF;AACnF,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAW3D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,EAAE,CAgBnE;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,iBAAiB,EAAE,CAoB1E"}
|