@adobe/helix-html-pipeline 1.1.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [1.3.0](https://github.com/adobe/helix-html-pipeline/compare/v1.2.1...v1.3.0) (2022-03-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * provide pipes for OPTIONS and POSTs ([#24](https://github.com/adobe/helix-html-pipeline/issues/24)) ([1dfc47e](https://github.com/adobe/helix-html-pipeline/commit/1dfc47e764a0b1d8acee80b51b845be2e54a16f5))
7
+
8
+ ## [1.2.1](https://github.com/adobe/helix-html-pipeline/compare/v1.2.0...v1.2.1) (2022-03-16)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * reject double-slashes ([#22](https://github.com/adobe/helix-html-pipeline/issues/22)) ([5aee75d](https://github.com/adobe/helix-html-pipeline/commit/5aee75d4109550525d971c64d87e4f2420863c30)), closes [#20](https://github.com/adobe/helix-html-pipeline/issues/20)
14
+
15
+ # [1.2.0](https://github.com/adobe/helix-html-pipeline/compare/v1.1.3...v1.2.0) (2022-03-16)
16
+
17
+
18
+ ### Features
19
+
20
+ * use hast instead of jsdom ([#12](https://github.com/adobe/helix-html-pipeline/issues/12)) ([bee0a0b](https://github.com/adobe/helix-html-pipeline/commit/bee0a0b3309919f896520bc700dd2d867be19a1c)), closes [#11](https://github.com/adobe/helix-html-pipeline/issues/11)
21
+
1
22
  ## [1.1.3](https://github.com/adobe/helix-html-pipeline/compare/v1.1.2...v1.1.3) (2022-03-12)
2
23
 
3
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-html-pipeline",
3
- "version": "1.1.3",
3
+ "version": "1.3.0",
4
4
  "description": "Helix HTML Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -35,8 +35,11 @@
35
35
  "@adobe/helix-markdown-support": "3.1.2",
36
36
  "@adobe/helix-shared-utils": "2.0.5",
37
37
  "github-slugger": "1.4.0",
38
+ "hast-util-raw": "7.2.1",
39
+ "hast-util-select": "5.0.1",
38
40
  "hast-util-to-html": "8.0.3",
39
- "jsdom": "19.0.0",
41
+ "hast-util-to-string": "2.0.0",
42
+ "hastscript": "7.0.2",
40
43
  "mdast-util-gfm-footnote": "1.0.1",
41
44
  "mdast-util-gfm-strikethrough": "1.0.1",
42
45
  "mdast-util-gfm-table": "1.0.3",
@@ -50,15 +53,17 @@
50
53
  "micromark-extension-gfm-task-list-item": "1.0.3",
51
54
  "micromark-util-combine-extensions": "1.0.0",
52
55
  "mime": "3.0.0",
53
- "property-information": "6.1.1",
56
+ "rehype-format": "4.0.1",
57
+ "rehype-minify-whitespace": "5.0.0",
58
+ "rehype-parse": "8.0.4",
54
59
  "remark-parse": "10.0.1",
55
60
  "strip-markdown": "5.0.0",
56
61
  "unified": "10.1.2",
57
62
  "unist-util-map": "3.0.0",
63
+ "unist-util-remove": "3.1.0",
58
64
  "unist-util-remove-position": "4.0.1",
59
65
  "unist-util-select": "4.0.1",
60
- "unist-util-visit": "4.1.0",
61
- "uri-js": "4.4.1"
66
+ "unist-util-visit": "4.1.0"
62
67
  },
63
68
  "devDependencies": {
64
69
  "@adobe/eslint-config-helix": "1.3.2",
@@ -75,19 +80,16 @@
75
80
  "eslint-plugin-header": "3.1.1",
76
81
  "eslint-plugin-import": "2.25.4",
77
82
  "esmock": "1.7.4",
78
- "hastscript": "7.0.2",
79
83
  "husky": "7.0.4",
80
- "hyperscript": "2.0.2",
81
84
  "js-yaml": "4.1.0",
82
85
  "jsdoc-to-markdown": "7.1.1",
86
+ "jsdom": "19.0.0",
83
87
  "junit-report-builder": "3.0.0",
84
88
  "lint-staged": "12.3.5",
85
89
  "mocha": "9.2.2",
86
90
  "mocha-multi-reporters": "1.5.1",
87
91
  "remark-gfm": "3.0.1",
88
- "semantic-release": "19.0.2",
89
- "sinon": "13.0.1",
90
- "unist-builder": "3.0.0"
92
+ "semantic-release": "19.0.2"
91
93
  },
92
94
  "lint-staged": {
93
95
  "*.js": "eslint",
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import {Node} from "unist";
13
13
  import GithubSlugger from 'github-slugger';
14
+ import { Root } from 'hast';
14
15
 
15
16
  declare enum SourceType {
16
17
  CONTENT = 'content',
@@ -50,12 +51,9 @@ declare class PipelineContent {
50
51
  mdast: Node;
51
52
 
52
53
  /**
53
- * document specific metadata
54
+ * The transformed document (hast) representation
54
55
  */
55
- meta: object;
56
- title: string;
57
- intro: string;
58
- image: string;
56
+ hast: Root;
59
57
 
60
58
  /**
61
59
  * slugger to use for heading id calculations
@@ -63,7 +61,10 @@ declare class PipelineContent {
63
61
  slugger: GithubSlugger;
64
62
 
65
63
  /**
66
- * The transformed document (jsom) representation
64
+ * document specific metadata
67
65
  */
68
- document: Document;
66
+ meta: object;
67
+ title: string;
68
+ intro: string;
69
+ image: string;
69
70
  }
@@ -9,6 +9,7 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
+ import GithubSlugger from 'github-slugger';
12
13
 
13
14
  /**
14
15
  * State of the pipeline
@@ -21,6 +22,7 @@ export class PipelineContent {
21
22
  constructor() {
22
23
  Object.assign(this, {
23
24
  sourceBus: 'content',
25
+ slugger: new GithubSlugger(),
24
26
  });
25
27
  }
26
28
  }
@@ -9,6 +9,8 @@
9
9
  * OF ANY KIND; either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
+ import { Element } from 'hast';
13
+
12
14
  declare interface PipelineResponseInit {
13
15
  status?: number;
14
16
  headers: Map<string, string> | object;
@@ -17,7 +19,10 @@ declare interface PipelineResponseInit {
17
19
  declare class PipelineResponse {
18
20
  constructor(body?:string, init?:PipelineResponseInit);
19
21
  status: number;
20
- document?: Document;
22
+ /**
23
+ * The transformed document (hast) representation
24
+ */
25
+ document: Element;
21
26
  body: string;
22
27
  headers: Map<string, string>;
23
28
  error: any;
@@ -9,12 +9,13 @@
9
9
  * OF ANY KIND; either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import {PathInfo, S3Loader, PipelineTimer} from "./index";
12
+ import {PathInfo, S3Loader, FormsMessageDispatcher, PipelineTimer} from "./index";
13
13
  import {PipelineContent} from "./PipelineContent";
14
14
 
15
15
  declare interface PipelineOptions {
16
16
  log: Console;
17
17
  s3Loader: S3Loader;
18
+ messageDispatcher: FormsMessageDispatcher;
18
19
  owner: string;
19
20
  repo: string;
20
21
  ref: string;
@@ -31,6 +32,7 @@ declare class PipelineState {
31
32
  content: PipelineContent;
32
33
  contentBusId: string;
33
34
  s3Loader: S3Loader;
35
+ messageDispatcher: FormsMessageDispatcher;
34
36
 
35
37
  /**
36
38
  * Content bus partition
@@ -36,6 +36,7 @@ export class PipelineState {
36
36
  helixConfig: undefined,
37
37
  metadata: undefined,
38
38
  s3Loader: opts.s3Loader,
39
+ messageDispatcher: opts.messageDispatcher,
39
40
  timer: opts.timer,
40
41
  });
41
42
  }
@@ -0,0 +1,160 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
+ import { PipelineResponse } from './PipelineResponse.js';
14
+ import fetchMetadata from './steps/fetch-metadata.js';
15
+ import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
16
+ import { getOriginalHost } from './steps/utils.js';
17
+
18
+ function error(log, msg, status, response) {
19
+ log.error(msg);
20
+ response.status = status;
21
+ response.headers.set('x-error', cleanupHeaderValue(msg));
22
+ return response;
23
+ }
24
+
25
+ /**
26
+ * Converts URLSearchParams to an object
27
+ * @param {URLSearchParams} searchParams the search params object
28
+ * @returns {Object} The converted object
29
+ */
30
+ function searchParamsToObject(searchParams) {
31
+ const result = {};
32
+
33
+ for (const key of searchParams.keys()) {
34
+ // get all values association with the key
35
+ const values = searchParams.getAll(key);
36
+
37
+ // if multiple values, convert to array
38
+ result[key] = (values.length === 1) ? values[0] : values;
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ /**
45
+ * Extracts and parses the body data from the request
46
+ * @param {PipelineRequest} request the request object (see fetch api)
47
+ * @returns {Object} The body data
48
+ * @throws {Error} If an error occurs parsing the body
49
+ */
50
+ export async function extractBodyData(request) {
51
+ let { body } = request;
52
+ if (!body) {
53
+ throw Error('missing body');
54
+ }
55
+ const type = request.headers.get('content-type');
56
+
57
+ // if content is form-urlencoded we need place the object in the body
58
+ // in a "data" property in the body as this is what forms-service expects.
59
+ if (/^application\/x-www-form-urlencoded/.test(type)) {
60
+ // did they pass an object in the body when a form-urlencoded body was expected?
61
+ if (body === '[object Object]') {
62
+ throw Error('invalid form-urlencoded body');
63
+ }
64
+
65
+ body = {
66
+ data: searchParamsToObject(new URLSearchParams(body)),
67
+ };
68
+
69
+ // else treat the body as json
70
+ } else if (/^application\/json/.test(type)) {
71
+ body = JSON.parse(body);
72
+ // verify the body data is as expected
73
+ if (!body.data) {
74
+ throw Error('missing body.data');
75
+ }
76
+ } else {
77
+ throw Error(`post body content-type not supported: ${type}`);
78
+ }
79
+ return body;
80
+ }
81
+
82
+ /**
83
+ * Handle a pipeline POST request.
84
+ * At this point POST's only apply to json files that are backed by a workbook.
85
+ * @param {PipelineState} state pipeline options
86
+ * @param {PipelineRequest} request
87
+ * @returns {Promise<PipelineResponse>} a response
88
+ */
89
+ export async function formsPipe(state, request) {
90
+ const { log } = state;
91
+
92
+ // todo: improve
93
+ const response = new PipelineResponse('', {
94
+ headers: {
95
+ 'content-type': 'text/plain; charset=utf-8',
96
+ },
97
+ });
98
+ await fetchMetadata(state, request, response);
99
+ await setCustomResponseHeaders(state, request, response);
100
+
101
+ const {
102
+ owner, repo, ref, contentBusId, partition, s3Loader,
103
+ } = state;
104
+ const { path } = state.info;
105
+ const resourcePath = `${path}.json`;
106
+
107
+ // block all POSTs to resources with extensions
108
+ if (state.info.originalExtension !== '') {
109
+ return error(log, 'POST to URL with extension not allowed', 405, response);
110
+ }
111
+
112
+ // head workbook in content bus
113
+ const resourceFetchResponse = await s3Loader.headObject('helix-content-bus', `${contentBusId}/${partition}${resourcePath}`);
114
+ if (resourceFetchResponse.status !== 200) {
115
+ return resourceFetchResponse;
116
+ }
117
+
118
+ let body;
119
+ try {
120
+ body = await extractBodyData(request);
121
+ } catch (err) {
122
+ return error(log, err.message, 400, response);
123
+ }
124
+
125
+ const sheets = resourceFetchResponse.headers.get('x-amz-meta-x-sheet-names');
126
+ if (!sheets) {
127
+ return error(log, `Target workbook at ${resourcePath} missing x-sheet-names header.`, 403, response);
128
+ }
129
+
130
+ const sourceLocation = resourceFetchResponse.headers.get('x-amz-meta-x-source-location');
131
+ const referer = request.headers.get('referer') || 'unknown';
132
+ const sheetNames = sheets.split(',');
133
+
134
+ if (!sourceLocation || !sheetNames.includes('incoming')) {
135
+ return error(log, `Target workbook at ${resourcePath} is not setup to intake data.`, 403, response);
136
+ }
137
+
138
+ // Send message to SQS if workbook contains and incoming
139
+ // sheet and the source location is not null
140
+ const host = getOriginalHost(request.headers);
141
+
142
+ const message = {
143
+ url: `https://${ref}--${repo}--${owner}.hlx.live${resourcePath}`,
144
+ body,
145
+ host,
146
+ sourceLocation,
147
+ referer,
148
+ };
149
+
150
+ try {
151
+ // Send message to forms queue
152
+ const { requestId, messageId } = await state.messageDispatcher.dispatch(message);
153
+ response.status = 201;
154
+ response.headers.set('x-request-id', requestId);
155
+ response.headers.set('x-message-id', messageId);
156
+ return response;
157
+ } catch (err) {
158
+ return error(log, `Failed to send message to forms queue: ${err}`, 500, response);
159
+ }
160
+ }
package/src/html-pipe.js CHANGED
@@ -62,11 +62,14 @@ export async function htmlPipe(state, req) {
62
62
  });
63
63
 
64
64
  try { // fetch config first, since we need to compute the content-bus-id from the fstab ...
65
+ state.timer?.update('config-fetch');
65
66
  await fetchConfig(state, req, res);
67
+
66
68
  // ...and apply the folder mapping
67
69
  await folderMapping(state, req, res);
68
70
 
69
71
  // load metadata and content in parallel
72
+ state.timer?.update('content-fetch');
70
73
  await Promise.all([
71
74
  fetchMetadata(state, req, res),
72
75
  fetchContent(state, req, res),
@@ -80,9 +83,12 @@ export async function htmlPipe(state, req) {
80
83
  }
81
84
 
82
85
  if (state.content.sourceBus === 'code') {
86
+ state.timer?.update('serialize');
83
87
  await renderCode(state, req, res);
84
88
  } else {
89
+ state.timer?.update('parse');
85
90
  await parseMarkdown(state);
91
+ state.timer?.update('render');
86
92
  await splitSections(state);
87
93
  await getMetadata(state); // this one extracts the metadata from the mdast
88
94
  await unwrapSoleImages(state);
@@ -96,6 +102,7 @@ export async function htmlPipe(state, req) {
96
102
  await addHeadingIds(state);
97
103
  await render(state, req, res);
98
104
  await removeHlxProps(state, req, res);
105
+ state.timer?.update('serialize');
99
106
  await tohtml(state, req, res);
100
107
  }
101
108
 
package/src/index.d.ts CHANGED
@@ -83,6 +83,19 @@ declare interface S3Loader {
83
83
  headObject(bucketId, key): Promise<PipelineResponse>;
84
84
  }
85
85
 
86
+ declare interface DispatchMessageResponse {
87
+ messageId:string,
88
+ requestId:string,
89
+ }
90
+
91
+ declare interface FormsMessageDispatcher {
92
+ /**
93
+ * Dispatches the message to the forms queue
94
+ * @param {object} message
95
+ */
96
+ dispatch(message:object): Promise<DispatchMessageResponse>;
97
+ }
98
+
86
99
  /**
87
100
  * Timer
88
101
  */
package/src/index.js CHANGED
@@ -11,6 +11,8 @@
11
11
  */
12
12
  export * from './html-pipe.js';
13
13
  export * from './json-pipe.js';
14
+ export * from './options-pipe.js';
15
+ export * from './forms-pipe.js';
14
16
  export * from './PipelineContent.js';
15
17
  export * from './PipelineRequest.js';
16
18
  export * from './PipelineResponse.js';
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { PipelineResponse } from './PipelineResponse.js';
13
+ import fetchMetadata from './steps/fetch-metadata.js';
14
+ import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
15
+
16
+ /**
17
+ * Handles options requests
18
+ * @param {PipelineState} state pipeline options
19
+ * @param {PipelineRequest} request
20
+ * @returns {Response} a response
21
+ */
22
+ export async function optionsPipe(state, request) {
23
+ // todo: improve
24
+ const response = new PipelineResponse('', {
25
+ status: 204,
26
+ headers: {
27
+ // Set preflight cache duration
28
+ 'access-control-max-age': '86400',
29
+ // Allow content type header
30
+ 'access-control-allow-headers': 'content-type',
31
+ },
32
+ });
33
+ await fetchMetadata(state, request, response);
34
+ await setCustomResponseHeaders(state, request, response);
35
+
36
+ return response;
37
+ }
@@ -9,6 +9,8 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
+ import { toString } from 'hast-util-to-string';
13
+ import { visit } from 'unist-util-visit';
12
14
 
13
15
  /**
14
16
  * Adds missing `id` attributes to the headings
@@ -16,17 +18,16 @@
16
18
  * @param {PipelineContent } content The current context of processing pipeline
17
19
  */
18
20
  export default async function fixSections({ content }) {
19
- const { slugger, document } = content;
20
- ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
21
- .forEach((tagName) => {
22
- document.querySelectorAll(tagName)
23
- .forEach(($h) => {
24
- if (!$h.id) {
25
- const text = $h.textContent.trim();
26
- if (text) {
27
- $h.setAttribute('id', slugger.slug(text));
28
- }
29
- }
30
- });
31
- });
21
+ const { slugger, hast } = content;
22
+ visit(hast, (node) => {
23
+ if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName)) {
24
+ const { properties } = node;
25
+ if (!properties.id) {
26
+ const text = toString(node).trim();
27
+ if (text) {
28
+ properties.id = slugger.slug(text);
29
+ }
30
+ }
31
+ }
32
+ });
32
33
  }
@@ -9,57 +9,57 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
+ import { h } from 'hastscript';
13
+ import { selectAll, select } from 'hast-util-select';
14
+ import { toString } from 'hast-util-to-string';
12
15
  import { toClassName } from './utils.js';
16
+ import { replace, childNodes } from '../utils/hast-utils.js';
13
17
 
14
18
  /**
15
19
  * Creates a "DIV representation" of a table.
16
20
  * @type PipelineStep
17
- * @param {Document} document
18
- * @param {HTMLTableElement} $table the table element
21
+ * @param {HTMLTTableElement} $table the table element
19
22
  * @returns {HTMLDivElement} the resulting div
20
23
  */
21
- function tableToDivs(document, $table) {
22
- const $cards = document.createElement('div');
23
-
24
- // iterate over the table to avoid problem with query selector and nested tables
24
+ function tableToDivs($table) {
25
+ const $cards = h('div');
25
26
  const $rows = [];
26
- if ($table.tHead) {
27
- $rows.push(...$table.tHead.rows);
28
- }
29
- for (const $tbody of $table.tBodies) {
30
- $rows.push(...$tbody.rows);
27
+ for (const child of $table.children) {
28
+ if (child.tagName === 'thead' || child.tagName === 'tbody') {
29
+ $rows.push(...childNodes(child));
30
+ }
31
31
  }
32
+
32
33
  if ($rows.length === 0) {
33
34
  return $cards;
34
35
  }
35
- const $headerRow = $rows.shift();
36
+ const $headerCols = childNodes($rows.shift());
36
37
 
37
38
  // special case, only 1 row and 1 column with a nested table
38
- if ($rows.length === 0 && $headerRow.cells.length === 1) {
39
- const $nestedTable = $headerRow.cells[0].querySelector(':scope table');
39
+ if ($rows.length === 0 && $headerCols.length === 1) {
40
+ const $nestedTable = select(':scope table', $headerCols[0]);
40
41
  if ($nestedTable) {
41
42
  return $nestedTable;
42
43
  }
43
44
  }
44
45
 
45
46
  // get columns names
46
- const clazz = Array.from($headerRow.cells)
47
- .map((e) => toClassName(e.textContent))
47
+ const clazz = $headerCols
48
+ .map((e) => toClassName(toString(e)))
48
49
  .filter((c) => !!c)
49
50
  .join('-');
50
51
  if (clazz) {
51
- $cards.classList.add(clazz);
52
+ $cards.properties.className = [clazz];
52
53
  }
53
54
 
54
55
  // construct page block
55
56
  for (const $row of $rows) {
56
- const $card = document.createElement('div');
57
- for (const $cell of $row.cells) {
58
- const $div = document.createElement('div');
59
- $div.append(...$cell.childNodes);
60
- $card.append($div);
57
+ const $card = h('div');
58
+ for (const $cell of childNodes($row)) {
59
+ // convert to div
60
+ $card.children.push(h('div', $cell.children));
61
61
  }
62
- $cards.append($card);
62
+ $cards.children.push($card);
63
63
  }
64
64
  return $cards;
65
65
  }
@@ -70,9 +70,10 @@ function tableToDivs(document, $table) {
70
70
  * @param context The current context of processing pipeline
71
71
  */
72
72
  export default function createPageBlocks({ content }) {
73
- const { document } = content;
74
- document.querySelectorAll('body > div > table').forEach(($table) => {
75
- const $div = tableToDivs(document, $table);
76
- $table.parentNode.replaceChild($div, $table);
73
+ const { hast } = content;
74
+ selectAll('div > table', hast).forEach(($table) => {
75
+ const $div = tableToDivs($table);
76
+ // replace child in parent
77
+ replace(hast, $table, $div);
77
78
  });
78
79
  }
@@ -9,6 +9,9 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
+ import { h } from 'hastscript';
13
+ import { selectAll } from 'hast-util-select';
14
+ import { replace } from '../utils/hast-utils.js';
12
15
  import { optimizeImageURL } from './utils.js';
13
16
 
14
17
  /**
@@ -17,19 +20,20 @@ import { optimizeImageURL } from './utils.js';
17
20
  * @param context The current context of processing pipeline
18
21
  */
19
22
  export default async function createPictures({ content }) {
20
- const { document } = content;
23
+ const { hast } = content;
21
24
 
22
25
  // transform <img> to <picture>
23
- document.querySelectorAll('img[src^="./media_"]').forEach((img, i) => {
24
- const picture = document.createElement('picture');
25
- const source = document.createElement('source');
26
- const src = img.getAttribute('src');
27
- source.setAttribute('media', '(max-width: 400px)');
28
- source.setAttribute('srcset', optimizeImageURL(src, 750));
29
- picture.appendChild(source);
30
- img.setAttribute('loading', i > 0 ? 'lazy' : 'eager'); // load all but first image lazy
31
- img.setAttribute('src', optimizeImageURL(src, 2000));
32
- img.parentNode.insertBefore(picture, img);
33
- picture.appendChild(img);
26
+ selectAll('img[src^="./media_"]', hast).forEach((img, i) => {
27
+ const { src } = img.properties;
28
+ const source = h('source');
29
+ source.properties.media = '(max-width: 400px)';
30
+ source.properties.srcset = optimizeImageURL(src, 750);
31
+
32
+ const picture = h('picture', source);
33
+ img.properties.loading = i > 0 ? 'lazy' : 'eager';
34
+ img.properties.src = optimizeImageURL(src, 2000);
35
+
36
+ replace(hast, img, picture);
37
+ picture.children.push(img);
34
38
  });
35
39
  }