Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Breaking changes for 2022 #57

Merged
merged 11 commits into from
May 25, 2022
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"micromark-util-combine-extensions": "1.0.0",
"mime": "3.0.0",
"rehype-format": "4.0.1",
"rehype-minify-whitespace": "5.0.1",
"rehype-parse": "8.0.4",
"remark-parse": "10.0.1",
"strip-markdown": "5.0.0",
Expand All @@ -61,7 +60,8 @@
"unist-util-remove": "3.1.0",
"unist-util-remove-position": "4.0.1",
"unist-util-select": "4.0.1",
"unist-util-visit": "4.1.0"
"unist-util-visit": "4.1.0",
"unist-util-visit-parents": "5.1.0"
},
"devDependencies": {
"@adobe/eslint-config-helix": "1.3.2",
Expand Down
4 changes: 2 additions & 2 deletions src/html-pipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import parseMarkdown from './steps/parse-markdown.js';
import removeHlxProps from './steps/removeHlxProps.js';
import render from './steps/render.js';
import renderCode from './steps/render-code.js';
import rewriteBlobImages from './steps/rewrite-blob-images.js';
import rewriteUrls from './steps/rewrite-urls.js';
import rewriteIcons from './steps/rewrite-icons.js';
import setXSurrogateKeyHeader from './steps/set-x-surrogate-key-header.js';
import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
Expand Down Expand Up @@ -94,7 +94,7 @@ export async function htmlPipe(state, req) {
await getMetadata(state); // this one extracts the metadata from the mdast
await unwrapSoleImages(state);
await html(state);
await rewriteBlobImages(state);
await rewriteUrls(state);
await rewriteIcons(state);
await fixSections(state);
await createPageBlocks(state);
Expand Down
15 changes: 6 additions & 9 deletions src/steps/create-page-blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { h } from 'hastscript';
import { selectAll, select } from 'hast-util-select';
import { toString } from 'hast-util-to-string';
import { toClassName } from './utils.js';
import { toBlockCSSClassNames } from './utils.js';
import { replace, childNodes } from '../utils/hast-utils.js';

/**
Expand Down Expand Up @@ -44,20 +44,17 @@ function tableToDivs($table) {
}

// get columns names
const clazz = $headerCols
.map((e) => toClassName(toString(e)))
.filter((c) => !!c)
.join('-');
if (clazz) {
$cards.properties.className = [clazz];
}
$cards.properties.className = toBlockCSSClassNames(toString($headerCols[0]));

// construct page block
for (const $row of $rows) {
const $card = h('div');
for (const $cell of childNodes($row)) {
// convert to div
$card.children.push(h('div', $cell.children));
$card.children.push(h('div', {
'data-align': $cell.properties.align,
'data-valign': $cell.properties.vAlign,
}, $cell.children));
}
$cards.children.push($card);
}
Expand Down
88 changes: 73 additions & 15 deletions src/steps/create-pictures.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,64 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import mime from 'mime';
import { parse } from 'querystring';
import { h } from 'hastscript';
import { selectAll } from 'hast-util-select';
import { replace } from '../utils/hast-utils.js';
import { optimizeImageURL } from './utils.js';
import { visitParents } from 'unist-util-visit-parents';

const BREAK_POINTS = [
{ media: '(min-width: 400px)', width: '2000' },
{ width: '750' },
];

export function createOptimizedPicture(src, alt = '', eager = false) {
const url = new URL(src, 'https://localhost/');
const { pathname, hash = '' } = url;
let { width, height } = parse(hash.substring(1)); // intrinsic dimensions
// detect bug in media handler that created fragments like `width=800&width=600`
if (Array.isArray(width)) {
[width, height] = width;
}
const ext = pathname.substring(pathname.lastIndexOf('.') + 1);
const type = mime.getType(pathname);

const variants = [
...BREAK_POINTS.map((br) => ({
...br,
ext: 'webply',
type: 'image/webp',
})),
...BREAK_POINTS.map((br) => ({
...br,
ext,
type,
}))];

const sources = variants.map((v, i) => {
const srcset = `.${pathname}?width=${v.width}&format=${v.ext}&optimize=medium`;
if (i < variants.length - 1) {
return h('source', {
type: v.type,
srcset,
media: v.media,
});
}
return h('img', {
loading: eager ? 'eager' : 'lazy',
alt,
type: v.type,
src: srcset,
width,
height,
});
});

return h('picture', sources);
}

function isMediaImage(node) {
return node.tagName === 'img' && node.properties?.src.startsWith('./media_');
}

/**
* Converts imgs to pictures
Expand All @@ -22,18 +76,22 @@ import { optimizeImageURL } from './utils.js';
export default async function createPictures({ content }) {
const { hast } = content;

// transform <img> to <picture>
selectAll('img[src^="./media_"]', hast).forEach((img, i) => {
const { src } = img.properties;
const source = h('source');
source.properties.media = '(max-width: 400px)';
source.properties.srcset = optimizeImageURL(src, 750);

const picture = h('picture', source);
img.properties.loading = i > 0 ? 'lazy' : 'eager';
img.properties.src = optimizeImageURL(src, 2000);
let first = true;
visitParents(hast, isMediaImage, (img, parents) => {
const { src, alt } = img.properties;
const picture = createOptimizedPicture(src, alt, first);
first = false;

replace(hast, img, picture);
picture.children.push(img);
// check if parent has style and unwrap if needed
const parent = parents[parents.length - 1];
const parentTag = parent.tagName;
if (parentTag === 'em' || parentTag === 'strong') {
const grand = parents[parents.length - 2];
const idx = grand.children.indexOf(parent);
grand.children[idx] = picture;
} else {
const idx = parent.children.indexOf(img);
parent.children[idx] = picture;
}
});
}
8 changes: 4 additions & 4 deletions src/steps/get-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/
import { select, selectAll } from 'unist-util-select';
import { toString as plain } from 'mdast-util-to-string';
import { rewriteBlobLink } from './utils.js';
import { rewriteUrl } from './utils.js';

function yaml(section) {
section.meta = selectAll('yaml', section)
Expand All @@ -35,12 +35,12 @@ function intro(section) {
section.intro = para ? plain(para) : '';
}

function image(section) {
function image(section, state) {
// selects the most prominent image of the section
// TODO: get a better measure of prominence than "first"
const img = select('image', section);
if (img) {
section.image = rewriteBlobLink(img.url);
section.image = rewriteUrl(state, img.url);
}
}

Expand Down Expand Up @@ -157,7 +157,7 @@ export default function getMetadata(state) {
}

[yaml, title, intro, image, sectiontype, fallback].forEach((fn) => {
sections.forEach(fn);
sections.forEach((section) => fn(section, state));
});

const img = sections.filter((section) => section.image)[0];
Expand Down
19 changes: 9 additions & 10 deletions src/steps/rewrite-icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,29 @@
* governing permissions and limitations under the License.
*/
/* eslint-disable no-param-reassign */
import { h, s } from 'hastscript';
import { h } from 'hastscript';
import { CONTINUE, visit } from 'unist-util-visit';

const REGEXP_ICON = /:(#?[a-zA-Z_-]+[a-zA-Z0-9]*):/g;
const REGEXP_ICON = /:(#?[a-z_-]+[a-z\d]*):/gi;

/**
* Create a <img> or <svg> icon dom element eg:
* `<img class="icon icon-smile" src="/icons/smile.svg"/>` or
* `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-smile"><use href="/icons.svg#smile"></use></svg>`
* Create a <span> icon element:
*
* `<span class="icon icon-smile"></span>`
*
* @param {string} value the identifier of the icon
*/
function createIcon(value) {
let name = encodeURIComponent(value);

// icon starts with #
if (name.startsWith('%23')) {
// todo: still support sprite sheets?
name = name.substring(3);
return s('svg', { class: `icon icon-${name}` }, [
s('use', { href: `/icons.svg#${name}` }),
]);
}

// create normal image
return h('img', { class: `icon icon-${name}`, src: `/icons/${name}.svg`, alt: `${name} icon` });
// create normal span
return h('span', { className: ['icon', `icon-${name}`] });
}

/**
Expand Down
32 changes: 22 additions & 10 deletions src/steps/rewrite-blob-images.js → src/steps/rewrite-urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,30 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { selectAll } from 'hast-util-select';
import { rewriteBlobLink } from './utils.js';

import { CONTINUE, visit } from 'unist-util-visit';
import { rewriteUrl } from './utils.js';

/**
* Rewrite blob store image URLs to /hlx_* URLs
*
* @type PipelineStep
* @param content
* Rewrites all A and IMG urls
* @param {PipelineState} state
*/
export default function rewrite({ content }) {
const { hast } = content;
selectAll('img', hast).forEach((img) => {
img.properties.src = rewriteBlobLink(img.properties.src);
export default async function rewriteUrls(state) {
const { content: { hast } } = state;

const els = {
a: 'href',
img: 'src',
};

visit(hast, (node) => {
if (node.type !== 'element') {
return CONTINUE;
}
const attr = els[node.tagName];
if (attr) {
node.properties[attr] = rewriteUrl(state, node.properties[attr]);
}
return CONTINUE;
});
}
27 changes: 2 additions & 25 deletions src/steps/stringify-response.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
* governing permissions and limitations under the License.
*/
import { toHtml } from 'hast-util-to-html';
// import rehypeFormat from 'rehype-format';
import rehypeMinifyWhitespace from 'rehype-minify-whitespace';
import { visit } from 'unist-util-visit';
import rehypeFormat from 'rehype-format';

/**
* Serializes the response document to HTML
Expand All @@ -30,28 +28,7 @@ export default function stringify(state, req, res) {
if (!doc) {
throw Error('no response document');
}

// TODO: for the next breaking release, pretty print the HTML with rehypeFormat.
// TODO: but for backward compatibility, output all on 1 line.
// rehypeFormat()(doc);

// due to a bug in rehype-minify-whitespace, script content is also minified to 1 line, which
// can result in errors https://github.com/rehypejs/rehype-minify/issues/44
// so we 'save' all text first and revert it afterwards
visit(doc, (node) => {
if (node.tagName === 'script' && node.children[0]?.type === 'text') {
node.children[0].savedValue = node.children[0].value;
}
});

rehypeMinifyWhitespace()(doc);

visit(doc, (node) => {
if (node.tagName === 'script' && node.children[0]?.type === 'text') {
node.children[0].value = node.children[0].savedValue;
delete node.children[0].savedValue;
}
});
rehypeFormat()(doc);

res.body = toHtml(doc, {
upperDoctype: true,
Expand Down
Loading