caniemail
Version:
HTML and CSS Feature Support for Email Clients from caniemail.com
193 lines • 8.64 kB
JavaScript
import { getProperty } from 'dot-prop';
import { ElementType } from 'htmlparser2';
import styleToObject from 'style-to-object';
import { getSupportType } from './clients.js';
import { getMatchingAtRuleTitles, getMatchingFunctionTitles, getMatchingKeywordTitles, getMatchingPropertyTitles, getMatchingPropertyValuePairTitles, getMatchingUnitTitles } from './css-titles.js';
import { getFeatures } from './features.js';
import { getMatchingAttributeTitles, getMatchingElementAttributePairTitles, getMatchingElementTitles } from './html-titles.js';
import { getMatchingPseudoSelectorTitles, getMatchingSelectorTitles } from './selectors.js';
import { LocationIndex } from './location.js';
const atRules = new Set([
'charset',
'custom-media',
'document',
'font-face',
'host',
'import',
'keyframes',
'keyframe',
'media',
'namespace',
'page',
'supports'
]);
const adjustPosition = (position, offset) => {
if (!position || !offset)
return position;
return {
start: {
line: position.start.line + offset.line - 1,
column: position.start.line === 1
? position.start.column + offset.column - 1
: position.start.column
},
end: {
line: position.end.line + offset.line - 1,
column: position.end.line === 1 ? position.end.column + offset.column - 1 : position.end.column
},
...(position.source && { source: position.source })
};
};
const checkFeatures = ({ clients, issues, titles, position }) => {
const { all: features } = getFeatures();
for (const title of [titles].flat()) {
for (const client of clients) {
const feature = features.get(title);
if (feature === void 0)
throw new RangeError(`Feature "${title}" not found.`);
const { stats } = feature;
const supportMap = getProperty(stats, client);
if (supportMap === void 0) {
throw new RangeError(`Feature "${title}" not found on "${client}".`);
}
const supportStatus = getSupportType(supportMap);
const notes = (supportStatus.noteNumbers ?? []).map((noteNumber) => feature.notes_by_num[String(noteNumber)]);
const support = supportStatus.type;
const issue = {
notes,
title,
position,
support
};
if (support === 'none') {
issues.errors.set(client, issue);
}
else if (support === 'partial') {
issues.warnings.set(client, issue);
}
}
}
};
const checkDeclarations = ({ clients, declarations, issues, offset }) => {
const { css: features } = getFeatures();
for (const declaration of declarations) {
const { property: propertyName, value: propertyValue, position } = declaration;
const adjustedPosition = adjustPosition(position, offset);
if (propertyName !== void 0 && features.get(propertyName) !== void 0) {
const propertyTitles = getMatchingPropertyTitles({ propertyName });
checkFeatures({ clients, issues, titles: propertyTitles, position: adjustedPosition });
}
if (propertyValue !== void 0) {
const functionTitles = getMatchingFunctionTitles({ propertyValue });
const keywordTitles = getMatchingKeywordTitles({ propertyValue });
const unitTitles = getMatchingUnitTitles({ propertyValue });
checkFeatures({ clients, issues, titles: functionTitles, position: adjustedPosition });
checkFeatures({ clients, issues, titles: keywordTitles, position: adjustedPosition });
checkFeatures({ clients, issues, titles: unitTitles, position: adjustedPosition });
}
if (propertyName !== void 0 && propertyValue !== void 0) {
const propertyValuePairTitles = getMatchingPropertyValuePairTitles({
propertyName,
propertyValue
});
checkFeatures({
clients,
issues,
titles: propertyValuePairTitles,
position: adjustedPosition
});
}
}
};
const checkSelectors = ({ clients, issues, selectors, position, offset }) => {
const adjustedPosition = adjustPosition(position, offset);
for (const selector of selectors) {
const pseudoSelectorTitles = getMatchingPseudoSelectorTitles({ selector });
const selectorTitles = getMatchingSelectorTitles({ selector });
checkFeatures({ clients, issues, titles: pseudoSelectorTitles, position: adjustedPosition });
checkFeatures({ clients, issues, titles: selectorTitles, position: adjustedPosition });
}
};
const checkHtmlNode = ({ clients, issues, node, locationIndex }) => {
const elementTitles = getMatchingElementTitles({ tagName: node.tagName });
const position = node.sourceCodeLocation ||
(node.startIndex !== null && node.endIndex !== null
? locationIndex.positionOf(node.startIndex, node.endIndex)
: undefined);
checkFeatures({ clients, issues, titles: elementTitles, position });
if (node.attributes !== void 0) {
const elementAttributePairTitles = getMatchingElementAttributePairTitles({
attributes: Object.fromEntries(node.attributes.map((attr) => [attr.name, attr.value])),
tagName: node.tagName
});
const attributeTitles = getMatchingAttributeTitles({
attributes: node.attributes.map((attr) => attr.name)
});
checkFeatures({ clients, issues, titles: attributeTitles, position });
checkFeatures({ clients, issues, titles: elementAttributePairTitles, position });
const styleAttr = node.attributes.find((attr) => attr.name === 'style');
if (styleAttr !== void 0) {
const styleObject = (styleToObject.default ?? styleToObject)(styleAttr.value);
if (styleObject !== null) {
// Get the style attribute location
const attrLoc = styleAttr.sourceCodeLocation;
const offset = attrLoc
? {
line: attrLoc.startLine,
column: attrLoc.startCol + 'style="'.length
}
: undefined;
const declarations = Object.entries(styleObject).map(([property, value]) => ({
property,
value: String(value),
position
}));
checkDeclarations({ clients, declarations, issues, offset });
}
}
}
if ('childNodes' in node) {
for (const childNode of node.childNodes) {
if (childNode.type === ElementType.Tag || childNode.type === ElementType.Style) {
checkHtmlNode({ clients, issues, node: childNode, locationIndex });
}
}
}
};
export const checkStylesheet = ({ clients, issues, stylesheet, offset }) => {
const matchedAtRules = [];
for (const stylesheetRule of stylesheet.stylesheet?.rules ?? []) {
if (stylesheetRule.type === 'rule') {
const rule = stylesheetRule;
const declarations = (rule.declarations ?? [])
.filter((declaration) => declaration.type !== 'comment')
.map((declaration) => ({
property: declaration.property,
value: declaration.value,
position: declaration.position ? { ...declaration.position } : undefined
}));
checkDeclarations({ clients, declarations, issues, offset });
checkSelectors({
clients,
issues,
selectors: rule.selectors ?? [],
position: rule.position ? { ...rule.position } : undefined,
offset
});
}
if (atRules.has(stylesheetRule.type)) {
matchedAtRules.push(stylesheetRule.type);
}
}
const atRuleTitles = getMatchingAtRuleTitles({ atRules: matchedAtRules });
checkFeatures({ clients, issues, titles: atRuleTitles });
};
export const checkHtml = ({ clients, issues, document, source }) => {
const locationIndex = new LocationIndex(source);
for (const childNode of document.childNodes) {
if (childNode.type === ElementType.Tag) {
checkHtmlNode({ clients, issues, node: childNode, locationIndex });
}
}
};
//# sourceMappingURL=checks.js.map