Skip to content

Commit

Permalink
Merge 4fb4f39 into a20e905
Browse files Browse the repository at this point in the history
  • Loading branch information
papandreou committed Jul 22, 2019
2 parents a20e905 + 4fb4f39 commit c66422e
Show file tree
Hide file tree
Showing 138 changed files with 8,975 additions and 13 deletions.
114 changes: 114 additions & 0 deletions lib/downloadGoogleFonts.js
@@ -0,0 +1,114 @@
const fontkit = require('fontkit');

const AssetGraph = require('assetgraph');
const getGoogleIdForFontProps = require('./getGoogleIdForFontProps');
const unicodeRange = require('./unicodeRange');

const formatOrder = ['woff2', 'woff', 'truetype', 'opentype'];

/**
* Webfont properties object containing the main differentiators for a separate font file.
* @typedef {Object} FontProps
* @property {string} font-family [CSS font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family), unquoted
* @property {string} font-weight [CSS font-weight](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight)
* @property {string} font-style [CSS font-weight](https://developer.mozilla.org/en-US/docs/Web/CSS/font-style)
*/

/**
* User agent strings by desired font format.
* @readonly
* @enum {string}
*/
const formatAgents = {
eot:
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)',
ttf: '',
woff:
'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; rv:11.0) like Gecko',
woff2:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393'
};

/**
* Download google fonts for self-hosting
*
* @async
* @param {FontProps} fontProps CSS font properties to get font for
* @param {Object} options
* @param {String[]} [options.formats=['woff2', 'woff']] List of formats that should be inclued in the output
* @param {String} [options.fontDisplay='swap'] CSS font-display value in returned CSS blocks
* @param {String} [options.text] Text to create a subset with
* @return {String} CSS asset with inlined google fonts
*/
async function downloadGoogleFonts(
fontProps,
{ formats = ['woff2', 'woff'], fontDisplay = 'swap', text } = {}
) {
const sortedFormats = [];

for (const format of formatOrder) {
if (formats.includes(format)) {
sortedFormats.push(format);
}
}

const result = {};
const googleFontId = getGoogleIdForFontProps(fontProps);
let fontCssUrl = `https://fonts.googleapis.com/css?family=${googleFontId}`;

if (text) {
fontCssUrl += `&text=${encodeURIComponent(text)}`;
}

result.src = await Promise.all(
sortedFormats.map(async format => {
const assetGraph = new AssetGraph();
assetGraph.teepee.headers['User-Agent'] = formatAgents[format];

const [cssAsset] = await assetGraph.loadAssets(fontCssUrl);

await assetGraph.populate();

const [fontRelation] = assetGraph.findRelations({
from: cssAsset,
type: 'CssFontFaceSrc'
});

fontRelation.node.each(decl => {
if (decl.prop !== 'src') {
result[decl.prop] = decl.value;
}
});

return [fontRelation.to, fontRelation.format];
})
);

if (!('unicode-range' in result)) {
const font = result.src[0][0];
result['unicode-range'] = unicodeRange(
fontkit.create(font.rawSrc).characterSet
);
}

result['font-display'] = fontDisplay;

// Output font face declaration object as CSS
const declarationStrings = [];

for (const [property, value] of Object.entries(result)) {
if (property !== 'src') {
declarationStrings.push(` ${property}: ${value};`);
}
}

const sources = result.src.map(([font, format]) => {
return `url('${font.dataUrl}') format('${format}')`;
});

declarationStrings.push(` src: \n ${sources.join(',\n ')};`);

return ['@font-face {', ...declarationStrings, '}'].join('\n');
}

module.exports = downloadGoogleFonts;
19 changes: 19 additions & 0 deletions lib/extractReferencedCustomPropertyNames.js
@@ -0,0 +1,19 @@
const postcssValuesParser = require('postcss-values-parser');

function extractReferencedCustomPropertyNames(cssValue) {
const tokens = postcssValuesParser(cssValue).tokens;
const customPropertyNames = new Set();
for (let i = 0; i < tokens.length - 3; i += 1) {
if (
tokens[i][1] === 'var' &&
tokens[i + 1][0] === '(' &&
tokens[i + 2][1] === '--' &&
tokens[i + 3][0] === 'word'
) {
customPropertyNames.add(`--${tokens[i + 3][1]}`);
}
}
return customPropertyNames;
}

module.exports = extractReferencedCustomPropertyNames;
54 changes: 54 additions & 0 deletions lib/findCustomPropertyDefinitions.js
@@ -0,0 +1,54 @@
const extractReferencedCustomPropertyNames = require('./extractReferencedCustomPropertyNames');

// Find all custom property definitions grouped by the custom properties they contribute to
function findCustomPropertyDefinitions(cssAssets) {
const definitionsByProp = {};
const incomingReferencesByProp = {};
for (const cssAsset of cssAssets) {
cssAsset.eachRuleInParseTree(cssRule => {
if (
cssRule.parent.type === 'rule' &&
cssRule.type === 'decl' &&
/^--/.test(cssRule.prop)
) {
(definitionsByProp[cssRule.prop] =
definitionsByProp[cssRule.prop] || new Set()).add(cssRule);
for (const customPropertyName of extractReferencedCustomPropertyNames(
cssRule.value
)) {
(incomingReferencesByProp[cssRule.prop] =
incomingReferencesByProp[cssRule.prop] || new Set()).add(
customPropertyName
);
}
}
});
}
const expandedDefinitionsByProp = {};
for (const prop of Object.keys(definitionsByProp)) {
expandedDefinitionsByProp[prop] = new Set();
const seenProps = new Set();
const queue = [prop];
while (queue.length > 0) {
const referencedProp = queue.shift();
if (!seenProps.has(referencedProp)) {
seenProps.add(referencedProp);
if (definitionsByProp[referencedProp]) {
for (const cssRule of definitionsByProp[referencedProp]) {
expandedDefinitionsByProp[prop].add(cssRule);
}
}
const incomingReferences = incomingReferencesByProp[referencedProp];
if (incomingReferences) {
for (const incomingReference of incomingReferences) {
queue.push(incomingReference);
}
}
}
}
}

return expandedDefinitionsByProp;
}

module.exports = findCustomPropertyDefinitions;
80 changes: 80 additions & 0 deletions lib/gatherStylesheetsWithPredicates.js
@@ -0,0 +1,80 @@
module.exports = function gatherStylesheetsWithPredicates(
assetGraph,
htmlAsset
) {
const assetStack = [];
const incomingMedia = [];
const conditionalCommentConditionStack = [];
const result = [];
(function traverse(asset, isWithinNotIeConditionalComment, isWithinNoscript) {
if (assetStack.includes(asset)) {
// Cycle detected
return;
} else if (!asset.isLoaded) {
return;
}
assetStack.push(asset);
for (const relation of assetGraph.findRelations({
from: asset,
type: {
$in: [
'HtmlStyle',
'CssImport',
'HtmlConditionalComment',
'HtmlNoscript'
]
}
})) {
if (relation.type === 'HtmlNoscript') {
traverse(relation.to, isWithinNotIeConditionalComment, true);
} else if (relation.type === 'HtmlConditionalComment') {
conditionalCommentConditionStack.push(relation.condition);
traverse(
relation.to,
isWithinNotIeConditionalComment ||
(relation.conditionalComments &&
relation.conditionalComments.length > 0),
isWithinNoscript
);
conditionalCommentConditionStack.pop();
} else {
const media = relation.media;
if (media) {
incomingMedia.push(media);
}
traverse(
relation.to,
isWithinNotIeConditionalComment ||
(relation.conditionalComments &&
relation.conditionalComments.length > 0),
isWithinNoscript
);
if (media) {
incomingMedia.pop();
}
}
}
assetStack.pop();
if (asset.type === 'Css') {
const predicates = {};
for (const incomingMedium of incomingMedia) {
predicates[`mediaQuery:${incomingMedium}`] = true;
}
for (const conditionalCommentCondition of conditionalCommentConditionStack) {
predicates[`conditionalComment:${conditionalCommentCondition}`] = true;
}
if (isWithinNoscript) {
predicates.script = false;
}
if (isWithinNotIeConditionalComment) {
predicates['conditionalComment:IE'] = false;
}
result.push({
text: asset.text,
predicates
});
}
})(htmlAsset);

return result;
};

0 comments on commit c66422e

Please sign in to comment.