@jsenv/ast 3.0.6 → 3.0.7

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@jsenv/ast",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "description": "Parse and transform HTML,CSS and JS",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/jsenv/jsenv-core",
8
+ "url": "https://github.com/jsenv/core",
9
9
  "directory": "packages/ast"
10
10
  },
11
11
  "publishConfig": {
@@ -33,12 +33,12 @@
33
33
  "test": "node --conditions=development ./scripts/test.mjs"
34
34
  },
35
35
  "dependencies": {
36
- "@babel/parser": "7.21.4",
36
+ "@babel/parser": "7.21.8",
37
37
  "acorn": "8.8.2",
38
38
  "acorn-import-assertions": "1.8.0",
39
39
  "acorn-walk": "8.2.0",
40
40
  "parse5": "7.1.2",
41
- "postcss": "8.4.22",
41
+ "postcss": "8.4.23",
42
42
  "postcss-value-parser": "4.2.0"
43
43
  }
44
44
  }
@@ -3,6 +3,12 @@ import { parseFragment } from "parse5"
3
3
  import { setHtmlNodeAttributes } from "./html_node_attributes.js"
4
4
  import { findHtmlNode } from "./html_search.js"
5
5
  import { analyzeScriptNode } from "./html_analysis.js"
6
+ import {
7
+ getIndentation,
8
+ increaseIndentation,
9
+ getHtmlNodeText,
10
+ setHtmlNodeText,
11
+ } from "./html_node_text.js"
6
12
 
7
13
  export const removeHtmlNode = (htmlNode) => {
8
14
  const { childNodes } = htmlNode.parentNode
@@ -21,81 +27,93 @@ export const injectHtmlNode = (htmlAst, node, jsenvPluginName = "jsenv") => {
21
27
  setHtmlNodeAttributes(node, {
22
28
  "jsenv-injected-by": jsenvPluginName,
23
29
  })
24
- const htmlHtmlNode = findChild(htmlAst, (node) => node.nodeName === "html")
25
- const bodyNode = findChild(htmlHtmlNode, (node) => node.nodeName === "body")
26
- return insertHtmlNodeAfter(node, bodyNode)
30
+ const htmlNode = findChild(htmlAst, (node) => node.nodeName === "html")
31
+ const bodyNode = findChild(htmlNode, (node) => node.nodeName === "body")
32
+ let after
33
+ if (node.nodeName !== "#text") {
34
+ // last child that is not a text
35
+ for (const child of bodyNode.childNodes) {
36
+ if (child.nodeName !== "#text") {
37
+ after = child
38
+ break
39
+ }
40
+ after = child
41
+ }
42
+ }
43
+ insertHtmlNodeAfter(node, after)
27
44
  }
28
45
 
29
- export const injectScriptNodeAsEarlyAsPossible = (
46
+ export const injectHtmlNodeAsEarlyAsPossible = (
30
47
  htmlAst,
31
- scriptNode,
48
+ node,
32
49
  jsenvPluginName = "jsenv",
33
50
  ) => {
34
- setHtmlNodeAttributes(scriptNode, {
51
+ setHtmlNodeAttributes(node, {
35
52
  "jsenv-injected-by": jsenvPluginName,
36
53
  })
37
- const isJsModule = analyzeScriptNode(scriptNode).type === "js_module"
38
- if (isJsModule) {
39
- const firstImportmapScript = findHtmlNode(htmlAst, (node) => {
40
- return (
41
- node.nodeName === "script" &&
42
- analyzeScriptNode(node).type === "importmap"
43
- )
44
- })
54
+ const isScript = node.nodeName === "script"
45
55
 
46
- if (firstImportmapScript) {
47
- const importmapParent = firstImportmapScript.parentNode
48
- const importmapSiblings = importmapParent.childNodes
49
- const nextSiblings = importmapSiblings.slice(
50
- importmapSiblings.indexOf(firstImportmapScript) + 1,
51
- )
52
- let after = firstImportmapScript
53
- for (const nextSibling of nextSiblings) {
54
- if (nextSibling.nodeName === "script") {
55
- return insertHtmlNodeBefore(scriptNode, importmapParent, nextSibling)
56
- }
57
- if (nextSibling.nodeName === "link") {
58
- after = nextSibling
56
+ if (isScript) {
57
+ const isJsModule = analyzeScriptNode(node).type === "js_module"
58
+ if (isJsModule) {
59
+ const firstImportmapScript = findHtmlNode(htmlAst, (node) => {
60
+ return (
61
+ node.nodeName === "script" &&
62
+ analyzeScriptNode(node).type === "importmap"
63
+ )
64
+ })
65
+
66
+ if (firstImportmapScript) {
67
+ const importmapParent = firstImportmapScript.parentNode
68
+ const importmapSiblings = importmapParent.childNodes
69
+ const nextSiblings = importmapSiblings.slice(
70
+ importmapSiblings.indexOf(firstImportmapScript) + 1,
71
+ )
72
+ let after = firstImportmapScript
73
+ for (const nextSibling of nextSiblings) {
74
+ if (nextSibling.nodeName === "script") {
75
+ insertHtmlNodeBefore(node, nextSibling)
76
+ return
77
+ }
78
+ if (nextSibling.nodeName === "link") {
79
+ after = nextSibling
80
+ }
59
81
  }
82
+ insertHtmlNodeAfter(node, after)
83
+ return
60
84
  }
61
- return insertHtmlNodeAfter(scriptNode, importmapParent, after)
62
- }
63
- }
64
- const headNode = findChild(htmlAst, (node) => node.nodeName === "html")
65
- .childNodes[0]
66
- let after = headNode.childNodes[0]
67
- for (const child of headNode.childNodes) {
68
- if (child.nodeName === "script") {
69
- return insertHtmlNodeBefore(scriptNode, headNode, child)
70
85
  }
71
- if (child.nodeName === "link") {
72
- after = child
86
+ const headNode = findChild(htmlAst, (node) => node.nodeName === "html")
87
+ .childNodes[0]
88
+ let after = headNode.childNodes[0]
89
+ for (const child of headNode.childNodes) {
90
+ if (child.nodeName === "script") {
91
+ insertHtmlNodeBefore(node, child)
92
+ return
93
+ }
94
+ if (child.nodeName === "link") {
95
+ after = child
96
+ }
73
97
  }
98
+ insertHtmlNodeAfter(node, after)
99
+ return
74
100
  }
75
- return insertHtmlNodeAfter(scriptNode, headNode, after)
101
+
102
+ injectHtmlNode(htmlAst, node)
103
+ return
76
104
  }
77
105
 
78
- export const insertHtmlNodeBefore = (
79
- nodeToInsert,
80
- futureParentNode,
81
- futureNextSibling,
82
- ) => {
106
+ export const insertHtmlNodeBefore = (nodeToInsert, futureNextSibling) => {
107
+ const futureParentNode = futureNextSibling.parentNode
83
108
  const { childNodes = [] } = futureParentNode
84
- const futureIndex = futureNextSibling
85
- ? childNodes.indexOf(futureNextSibling)
86
- : 0
109
+ const futureIndex = childNodes.indexOf(futureNextSibling)
87
110
  injectWithWhitespaces(nodeToInsert, futureParentNode, futureIndex)
88
111
  }
89
112
 
90
- export const insertHtmlNodeAfter = (
91
- nodeToInsert,
92
- futureParentNode,
93
- futurePrevSibling,
94
- ) => {
113
+ export const insertHtmlNodeAfter = (nodeToInsert, futurePrevSibling) => {
114
+ const futureParentNode = futurePrevSibling.parentNode
95
115
  const { childNodes = [] } = futureParentNode
96
- const futureIndex = futurePrevSibling
97
- ? childNodes.indexOf(futurePrevSibling) + 1
98
- : childNodes.length
116
+ const futureIndex = childNodes.indexOf(futurePrevSibling) + 1
99
117
  injectWithWhitespaces(nodeToInsert, futureParentNode, futureIndex)
100
118
  }
101
119
 
@@ -104,30 +122,40 @@ const injectWithWhitespaces = (nodeToInsert, futureParentNode, futureIndex) => {
104
122
  const previousSiblings = childNodes.slice(0, futureIndex)
105
123
  const nextSiblings = childNodes.slice(futureIndex)
106
124
  const futureChildNodes = []
107
- const previousSibling = previousSiblings[previousSiblings.length - 1]
108
- if (previousSibling) {
125
+
126
+ if (previousSiblings.length) {
109
127
  futureChildNodes.push(...previousSiblings)
110
128
  }
111
- if (!previousSibling || !isWhitespaceNode(previousSibling)) {
129
+ const previousSibling = childNodes[futureIndex - 1]
130
+ if (
131
+ nodeToInsert.nodeName !== "#text" &&
132
+ (!previousSibling || !isWhitespaceNode(previousSibling))
133
+ ) {
134
+ let indentation
135
+ if (childNodes.length) {
136
+ indentation = getIndentation(childNodes[childNodes.length - 1])
137
+ } else {
138
+ const parentIndentation = getIndentation(futureParentNode)
139
+ indentation = increaseIndentation(parentIndentation, 2)
140
+ }
112
141
  futureChildNodes.push({
113
142
  nodeName: "#text",
114
- value: "\n ",
143
+ value: `\n${indentation}`,
115
144
  parentNode: futureParentNode,
116
145
  })
117
146
  }
118
147
  futureChildNodes.push(nodeToInsert)
119
- const nextSibling = nextSiblings[0]
120
- if (!nextSibling || !isWhitespaceNode(nextSibling)) {
121
- futureChildNodes.push({
122
- nodeName: "#text",
123
- value: "\n ",
124
- parentNode: futureParentNode,
125
- })
126
- }
127
- if (nextSibling) {
148
+ nodeToInsert.parentNode = futureParentNode
149
+ if (nextSiblings.length) {
128
150
  futureChildNodes.push(...nextSiblings)
129
151
  }
130
152
  futureParentNode.childNodes = futureChildNodes
153
+
154
+ // update indentation when node contains text
155
+ const text = getHtmlNodeText(nodeToInsert)
156
+ if (text) {
157
+ setHtmlNodeText(nodeToInsert, text, { indentation: "auto" })
158
+ }
131
159
  }
132
160
 
133
161
  const isWhitespaceNode = (node) => {
@@ -1,9 +1,17 @@
1
+ import {
2
+ getHtmlNodeAttribute,
3
+ setHtmlNodeAttributes,
4
+ } from "./html_node_attributes.js"
5
+
1
6
  export const getHtmlNodeText = (htmlNode) => {
2
7
  const textNode = getTextNode(htmlNode)
3
8
  return textNode ? textNode.value : undefined
4
9
  }
5
10
 
6
11
  const getTextNode = (htmlNode) => {
12
+ if (htmlNode.nodeName === "#text") {
13
+ return null
14
+ }
7
15
  const firstChild = htmlNode.childNodes[0]
8
16
  const textNode =
9
17
  firstChild && firstChild.nodeName === "#text" ? firstChild : null
@@ -17,7 +25,24 @@ export const removeHtmlNodeText = (htmlNode) => {
17
25
  }
18
26
  }
19
27
 
20
- export const setHtmlNodeText = (htmlNode, textContent) => {
28
+ export const setHtmlNodeText = (
29
+ htmlNode,
30
+ textContent,
31
+ { indentation } = {},
32
+ ) => {
33
+ if (indentation === "auto") {
34
+ const contentIndentedAttribute = getHtmlNodeAttribute(
35
+ htmlNode,
36
+ "content-indented",
37
+ )
38
+ if (contentIndentedAttribute === undefined) {
39
+ const indentation = getIndentation(htmlNode)
40
+ textContent = setIndentation(textContent, indentation)
41
+ setHtmlNodeAttributes(htmlNode, {
42
+ "content-indented": "",
43
+ })
44
+ }
45
+ }
21
46
  const textNode = getTextNode(htmlNode)
22
47
  if (textNode) {
23
48
  textNode.value = textContent
@@ -30,3 +55,64 @@ export const setHtmlNodeText = (htmlNode, textContent) => {
30
55
  htmlNode.childNodes.splice(0, 0, newTextNode)
31
56
  }
32
57
  }
58
+
59
+ export const getIndentation = (htmlNode) => {
60
+ const parentNode = htmlNode.parentNode
61
+ if (!parentNode) {
62
+ return ""
63
+ }
64
+
65
+ const siblings = parentNode.childNodes
66
+ const index = siblings.indexOf(htmlNode)
67
+ if (index === 0) {
68
+ if (htmlNode.nodeName === "#text") {
69
+ return extractIndentation(htmlNode)
70
+ }
71
+ return ""
72
+ }
73
+
74
+ let textNodeIndex = 0
75
+ let textNode
76
+ while (textNodeIndex !== index) {
77
+ const previousSibling = siblings[textNodeIndex]
78
+ textNodeIndex++
79
+ if (previousSibling.nodeName === "#text") {
80
+ textNode = previousSibling
81
+ break
82
+ }
83
+ }
84
+ if (!textNode) {
85
+ return ""
86
+ }
87
+
88
+ return extractIndentation(textNode)
89
+ }
90
+
91
+ const extractIndentation = (textNode) => {
92
+ const text = textNode.value
93
+ const lines = text.split(/\r?\n/)
94
+ const lastLine = lines[lines.length - 1]
95
+ if (lastLine.match(/^[\t ]+$/)) {
96
+ return lastLine
97
+ }
98
+ return ""
99
+ }
100
+
101
+ const setIndentation = (htmlNodeText, indentation) => {
102
+ const contentIdentation = increaseIndentation(indentation, 2)
103
+ const lines = htmlNodeText.trimEnd().split(/\r?\n/)
104
+ let result = `\n`
105
+ let i = 0
106
+ while (i < lines.length) {
107
+ const line = lines[i]
108
+ i++
109
+ result += `${contentIdentation}${line}\n`
110
+ }
111
+ result += `${indentation}`
112
+ return result
113
+ }
114
+
115
+ export const increaseIndentation = (indentation, size) => {
116
+ const char = indentation[0]
117
+ return char ? `${indentation}${char.repeat(size)}` : " ".repeat(size)
118
+ }
@@ -1,5 +1,6 @@
1
1
  import { parse, serialize, parseFragment } from "parse5"
2
2
 
3
+ import { insertHtmlNodeAfter, insertHtmlNodeBefore } from "./html_node.js"
3
4
  import {
4
5
  getHtmlNodeAttribute,
5
6
  setHtmlNodeAttributes,
@@ -40,14 +41,29 @@ export const parseHtmlString = (
40
41
  })
41
42
  }
42
43
  }
44
+ const htmlNode = htmlAst.childNodes.find((node) => node.nodeName === "html")
45
+ if (htmlNode) {
46
+ const bodyNode = htmlNode.childNodes.find(
47
+ (node) => node.nodeName === "body",
48
+ )
49
+ // for some reason "parse5" adds "\n\n" to the last text node of <body>
50
+ const lastBodyNode = bodyNode.childNodes[bodyNode.childNodes.length - 1]
51
+ if (
52
+ lastBodyNode &&
53
+ lastBodyNode.nodeName === "#text" &&
54
+ lastBodyNode.value.endsWith("\n\n")
55
+ ) {
56
+ lastBodyNode.value = lastBodyNode.value.slice(0, -2)
57
+ }
58
+ }
43
59
  return htmlAst
44
60
  }
45
61
 
46
62
  export const stringifyHtmlAst = (
47
63
  htmlAst,
48
- { cleanupJsenvAttributes = false } = {},
64
+ { cleanupJsenvAttributes = false, cleanupPositionAttributes = false } = {},
49
65
  ) => {
50
- if (cleanupJsenvAttributes) {
66
+ if (cleanupJsenvAttributes || cleanupPositionAttributes) {
51
67
  const htmlNode = findHtmlChildNode(
52
68
  htmlAst,
53
69
  (node) => node.nodeName === "html",
@@ -66,21 +82,77 @@ export const stringifyHtmlAst = (
66
82
  "original-position": undefined,
67
83
  "original-src-position": undefined,
68
84
  "original-href-position": undefined,
69
- "inlined-from-src": undefined,
70
- "inlined-from-href": undefined,
71
- "jsenv-cooked-by": undefined,
72
- "jsenv-inlined-by": undefined,
73
- "jsenv-injected-by": undefined,
74
- "jsenv-debug": undefined,
85
+ ...(cleanupJsenvAttributes
86
+ ? {
87
+ "inlined-from-src": undefined,
88
+ "inlined-from-href": undefined,
89
+ "jsenv-cooked-by": undefined,
90
+ "jsenv-inlined-by": undefined,
91
+ "jsenv-injected-by": undefined,
92
+ "jsenv-debug": undefined,
93
+ "content-indented": undefined,
94
+ }
95
+ : {}),
75
96
  })
76
97
  },
77
98
  })
78
99
  }
79
100
  }
101
+ // ensure body and html have \n
102
+ ensureLineBreaksBetweenHtmlNodes(htmlAst)
80
103
  const htmlString = serialize(htmlAst)
81
104
  return htmlString
82
105
  }
83
106
 
107
+ const ensureLineBreaksBetweenHtmlNodes = (rootNode) => {
108
+ const mutations = []
109
+
110
+ const documentType = rootNode.childNodes[0]
111
+ if (documentType.nodeName === "#documentType") {
112
+ const html = rootNode.childNodes[1]
113
+ if (html.nodeName === "html") {
114
+ mutations.push(() => {
115
+ insertHtmlNodeAfter({ nodeName: "#text", value: "\n" }, documentType)
116
+ })
117
+
118
+ const head = html.childNodes[0]
119
+ if (head.nodeName === "head") {
120
+ mutations.push(() => {
121
+ insertHtmlNodeBefore({ nodeName: "#text", value: "\n " }, head)
122
+ })
123
+ }
124
+
125
+ const body = html.childNodes.find((node) => node.nodeName === "body")
126
+ if (body) {
127
+ const bodyLastChild = body.childNodes[body.childNodes.length - 1]
128
+ if (bodyLastChild.nodeName !== "#text") {
129
+ mutations.push(() => {
130
+ insertHtmlNodeAfter(
131
+ { nodeName: "#text", value: "\n " },
132
+ bodyLastChild,
133
+ )
134
+ })
135
+ }
136
+ if (
137
+ bodyLastChild.nodeName === "#text" &&
138
+ bodyLastChild.value[bodyLastChild.value.length - 1] === "\n"
139
+ ) {
140
+ bodyLastChild.value = bodyLastChild.value.slice(0, -1)
141
+ }
142
+ }
143
+
144
+ const htmlLastChild = html.childNodes[html.childNodes.length - 1]
145
+ if (htmlLastChild.nodeName !== "#text") {
146
+ mutations.push(() => {
147
+ insertHtmlNodeAfter({ nodeName: "#text", value: "\n" }, htmlLastChild)
148
+ })
149
+ }
150
+ }
151
+ }
152
+
153
+ mutations.forEach((m) => m())
154
+ }
155
+
84
156
  export const parseSvgString = (svgString) => {
85
157
  const svgAst = parseFragment(svgString, {
86
158
  sourceCodeLocationInfo: true,
package/src/main.js CHANGED
@@ -21,7 +21,7 @@ export {
21
21
  removeHtmlNode,
22
22
  createHtmlNode,
23
23
  injectHtmlNode,
24
- injectScriptNodeAsEarlyAsPossible,
24
+ injectHtmlNodeAsEarlyAsPossible,
25
25
  insertHtmlNodeAfter,
26
26
  } from "./html/html_node.js"
27
27
  export {