build(aio): auto-fill width/height to all image tags

Parse all `<img>` tags, during doc-gen, and insert the width and height of
the sourceed image, if neither are already specified.

Warnings are reported if the `<img>` tag has no `src` attribute or the image
cannot be loaded.

The work is done in the `addImageDimensions` post-processor, which must be
configured with a `basePath` so that it knows where to find the images.

Closes #15888
This commit is contained in:
Peter Bacon Darwin
2017-04-28 15:05:05 +01:00
committed by Matias Niemelä
parent 64335d3521
commit ca17d4f639
7 changed files with 177 additions and 5 deletions

View File

@ -34,6 +34,9 @@ module.exports = new Package('angular-base', [
.factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); })
.factory(require('./readers/json'))
.factory(require('./services/copyFolder'))
.factory(require('./services/getImageDimensions'))
.factory(require('./post-processors/add-image-dimensions'))
.config(function(checkAnchorLinksProcessor) {
// This is disabled here to prevent false negatives for the `docs-watch` task.
@ -120,9 +123,11 @@ module.exports = new Package('angular-base', [
})
.config(function(postProcessHtml) {
.config(function(postProcessHtml, addImageDimensions) {
addImageDimensions.basePath = path.resolve(AIO_PATH, 'src');
postProcessHtml.plugins = [
require('./post-processors/autolink-headings')
require('./post-processors/autolink-headings'),
addImageDimensions
];
})

View File

@ -0,0 +1,40 @@
const visit = require('unist-util-visit');
const is = require('hast-util-is-element');
const source = require('unist-util-source');
/**
* Add the width and height of the image to the `img` tag if they are
* not already provided. This helps prevent jank when the page is
* rendered before the image has downloaded.
*
* If there is no `src` attribute on an image, or it is not possible
* to load the image file indicated by the `src` then a warning is emitted.
*/
module.exports = function addImageDimensions(getImageDimensions) {
return function addImageDimensionsImpl() {
return (ast, file) => {
visit(ast, node => {
if (is(node, 'img')) {
const props = node.properties;
const src = props.src;
if (!src) {
file.message('Missing src in image tag `' + source(node, file) + '`');
} else if (props.width === undefined && props.height === undefined) {
try {
const dimensions = getImageDimensions(addImageDimensionsImpl.basePath, src);
props.width = '' + dimensions.width;
props.height = '' + dimensions.height;
} catch(e) {
if (e.code === 'ENOENT') {
file.message('Unable to load src in image tag `' + source(node, file) + '`');
} else {
file.fail(e.message);
}
}
}
}
});
};
};
};

View File

@ -0,0 +1,110 @@
var createTestPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni');
describe('addImageDimensions post-processor', () => {
let processor, getImageDimensionsSpy, addImageDimensions, log;
beforeEach(() => {
const testPackage = createTestPackage('angular-base-package')
.factory('getImageDimensions', mockGetImageDimensions);
const dgeni = new Dgeni([testPackage]);
const injector = dgeni.configureInjector();
log = injector.get('log');
addImageDimensions = injector.get('addImageDimensions');
addImageDimensions.basePath = 'base/path';
getImageDimensionsSpy = injector.get('getImageDimensions');
processor = injector.get('postProcessHtml');
processor.docTypes = ['a'];
processor.plugins = [addImageDimensions];
});
it('should add the image dimensions into <img> tags', () => {
const docs = [{
docType: 'a',
renderedContent: `
<p>xxx</p>
<img src="a/b.jpg">
<p>yyy</p>
<img src="c/d.png">
<p>zzz</p>
`
}];
processor.$process(docs);
expect(getImageDimensionsSpy).toHaveBeenCalledWith('base/path', 'a/b.jpg');
expect(getImageDimensionsSpy).toHaveBeenCalledWith('base/path', 'c/d.png');
expect(docs).toEqual([{
docType: 'a',
renderedContent: `
<p>xxx</p>
<img src="a/b.jpg" width="10" height="20">
<p>yyy</p>
<img src="c/d.png" width="30" height="40">
<p>zzz</p>
`
}]);
});
it('should log a warning for images with no src attribute', () => {
const docs = [{
docType: 'a',
renderedContent: '<img attr="value">'
}];
processor.$process(docs);
expect(getImageDimensionsSpy).not.toHaveBeenCalled();
expect(docs).toEqual([{
docType: 'a',
renderedContent: '<img attr="value">'
}]);
expect(log.warn).toHaveBeenCalled();
});
it('should log a warning for images whose source cannot be loaded', () => {
getImageDimensionsSpy.and.callFake(() => {
const error = new Error('no such file or directory');
error.code = 'ENOENT';
throw error;
});
const docs = [{
docType: 'a',
renderedContent: '<img src="missing">'
}];
processor.$process(docs);
expect(getImageDimensionsSpy).toHaveBeenCalled();
expect(docs).toEqual([{
docType: 'a',
renderedContent: '<img src="missing">'
}]);
expect(log.warn).toHaveBeenCalled();
});
it('should ignore images with width or height attributes', () => {
const docs = [{
docType: 'a',
renderedContent: `
<img src="a/b.jpg" width="10">
<img src="c/d.jpg" height="10">
<img src="e/f.jpg" width="10" height="10">
`
}];
processor.$process(docs);
expect(getImageDimensionsSpy).not.toHaveBeenCalled();
expect(docs).toEqual([{
docType: 'a',
renderedContent: `
<img src="a/b.jpg" width="10">
<img src="c/d.jpg" height="10">
<img src="e/f.jpg" width="10" height="10">
`
}]);
});
function mockGetImageDimensions() {
const imageInfo = {
'a/b.jpg': { width: 10, height: 20 },
'c/d.png': { width: 30, height: 40 },
};
// eslint-disable-next-line jasmine/no-unsafe-spy
return jasmine.createSpy('getImageDimensions')
.and.callFake((base, url) => imageInfo[url]);
}
});

View File

@ -1,6 +1,7 @@
module.exports = function copyContentAssetsProcessor(copyFolder) {
return {
$runBefore: ['postProcessHtml'],
assetMappings: [],
$process() {
this.assetMappings.forEach(map => {

View File

@ -0,0 +1,6 @@
const { resolve } = require('canonical-path');
const sizeOf = require('image-size');
module.exports = function getImageDimensions() {
return (basePath, path) => sizeOf(resolve(basePath, path));
};