/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const PathResolver = require('../PathResolver');
const {fetchImageDimensions} = require('../fetchImageDimensions');
const {remove, insertAfter, createElement, firstChildByTag, nextNode} = require('../NodeUtils');
const DEFAULT_LAYOUT = 'intrinsic';
const LAYOUT_MIN_WIDTH = 320; // minimum mobile device screen width
/**
* Markdown - ensures markdown compatibility for input HTML
*
* This transformer adds out-of-the-box markdown support. This allows
* using AMP Optimizer to convert HTML documents created from Markdown
* files into valid AMP. A typical conversion flow would be:
*
* README.md => HTML => AMP Optimizer => valid AMP
*
* The only thing this transformer does is converting `
` tags into
* either `amp-img` or `amp-anim` tags. All other Markdown features are
* already supported by AMP. The transformer will try to resolve image
* dimensions from the actual files. Images larger than 320px will automatically
* get an intrinsic layout. For image detection to work, an optional dependency
* `probe-image-size` needs to be installed via NPM.
*
* This transformer supports the following options:
*
* - `markdown [Boolean]`: enables Markdown HTML support. Default is `false`.
* - `imageBasePath`: specifies a base path used to resolve an image during build,
* this can be a file system path or URL prefix.You can also pass a function
* `(imgSrc, params) => '../img/' + imgSrc` for dynamically calculating the image path.
*/
class Markdown {
constructor(config) {
this.log = config.log;
this.enabled = !!config.markdown;
// used for resolving image files
this.pathResolver = new PathResolver(config.imageBasePath);
}
async transform(tree, params) {
if (!this.enabled) {
return;
}
const html = firstChildByTag(tree, 'html');
if (!html) {
return;
}
const body = firstChildByTag(html, 'body');
if (!body) {
return;
}
let node = body;
const promises = [];
while (node) {
const tmpNode = nextNode(node);
if (node.tagName === 'img') {
promises.push(this.transformImg(node, params));
}
if(node.tagName === 'picture') {
promises.push(this.transformPicture(node, params));
}
node = tmpNode;
}
return Promise.all(promises);
}
async transformPicture(pictureNode, params) {
const imgNode = firstChildByTag(pictureNode, 'img');
if (!imgNode) {
return;
}
const src = imgNode.attribs && imgNode.attribs.src;
if (!src) {
return;
}
const resolvedSrc = this.pathResolver.resolve(src, params);
let dimensions;
try {
dimensions = await fetchImageDimensions(resolvedSrc);
} catch (error) {
this.log.warn(error.message);
// don't convert images we cannot resolve
return;
}
const ampImgOrAmpAnim = this.createAmpImgOrAmpAnim(dimensions, imgNode);
insertAfter(pictureNode.parent, ampImgOrAmpAnim, pictureNode);
remove(pictureNode);
}
async transformImg(imgNode, params) {
const src = imgNode.attribs && imgNode.attribs.src;
if (!src) {
return;
}
const resolvedSrc = this.pathResolver.resolve(src, params);
let dimensions;
try {
dimensions = await fetchImageDimensions(resolvedSrc);
} catch (error) {
this.log.warn(error.message);
// don't convert images we cannot resolve
return;
}
const ampImgOrAmpAnim = this.createAmpImgOrAmpAnim(dimensions, imgNode);
insertAfter(imgNode.parent, ampImgOrAmpAnim, imgNode);
remove(imgNode);
}
createAmpImgOrAmpAnim(dimensions, imgNode) {
const ampType = dimensions.type === 'gif' ? 'amp-anim' : 'amp-img';
const ampNode = createElement(ampType, imgNode.attribs);
// keep height and width if already specified
ampNode.attribs.width = imgNode.attribs.width || String(dimensions.width);
ampNode.attribs.height = imgNode.attribs.height || String(dimensions.height);
this.addLayout(ampNode, dimensions);
return ampNode;
}
addLayout(node, dimensions) {
if (dimensions.width < LAYOUT_MIN_WIDTH) {
return;
}
node.attribs.layout = DEFAULT_LAYOUT;
}
}
module.exports = Markdown;