@teleporthq/teleport-project-plugin-parse-embed 0.44.0-alpha.0 → 0.44.1
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/__tests__/component-plugin.ts +152 -0
- package/dist/cjs/component-plugin.d.ts +18 -0
- package/dist/cjs/component-plugin.d.ts.map +1 -1
- package/dist/cjs/component-plugin.js +109 -9
- package/dist/cjs/component-plugin.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +1 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/component-plugin.d.ts +18 -0
- package/dist/esm/component-plugin.d.ts.map +1 -1
- package/dist/esm/component-plugin.js +107 -8
- package/dist/esm/component-plugin.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +1 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +32 -32
- package/src/component-plugin.ts +123 -8
- package/src/utils.ts +3 -2
package/src/component-plugin.ts
CHANGED
|
@@ -7,21 +7,89 @@ interface ParseEmbedPluginConfig {
|
|
|
7
7
|
projectType: SUPPORTED_PROJECT_TYPES
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const SCRIPT_CLOSE_PH = '__THQ_SC__'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Replace all `</script>` except the last one with a placeholder so
|
|
14
|
+
* hast-util-from-html doesn't prematurely close the script element
|
|
15
|
+
* when the JS code inside contains a nested `</script>` (e.g. in a
|
|
16
|
+
* template-literal string).
|
|
17
|
+
*/
|
|
18
|
+
function escapeNestedScriptClose(html: string): string {
|
|
19
|
+
const regex = /<\/script\s*>/gi
|
|
20
|
+
const indices: Array<{ idx: number; len: number }> = []
|
|
21
|
+
let m = regex.exec(html)
|
|
22
|
+
while (m !== null) {
|
|
23
|
+
indices.push({ idx: m.index, len: m[0].length })
|
|
24
|
+
m = regex.exec(html)
|
|
25
|
+
}
|
|
26
|
+
if (indices.length <= 1) {
|
|
27
|
+
return html
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let result = ''
|
|
31
|
+
let lastEnd = 0
|
|
32
|
+
for (let i = 0; i < indices.length - 1; i++) {
|
|
33
|
+
result += html.substring(lastEnd, indices[i].idx)
|
|
34
|
+
result += SCRIPT_CLOSE_PH
|
|
35
|
+
lastEnd = indices[i].idx + indices[i].len
|
|
36
|
+
}
|
|
37
|
+
result += html.substring(lastEnd)
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* hast-util-to-jsx-inline-script wraps each <script> body in an outer {`…`}
|
|
43
|
+
* template literal with NO escaping of its own (its <style> branch goes through
|
|
44
|
+
* JSON.stringify; the script branch never got that treatment). A raw backtick
|
|
45
|
+
* or `${` in the body therefore terminates/interpolates the OUTER literal and
|
|
46
|
+
* the generated component does not compile.
|
|
47
|
+
*
|
|
48
|
+
* Escape both here, parity-aware and idempotent — the same algorithm as
|
|
49
|
+
* teleport-plugin-common's addRawAttributeToJSXTag (the non-advanced-embeds
|
|
50
|
+
* React path): a backtick behind an ODD number of backslashes is already
|
|
51
|
+
* escaped and is left alone, so content that editors pre-escaped keeps exactly
|
|
52
|
+
* one level of escaping, while raw content (the canonical storage form) gains
|
|
53
|
+
* the one level it needs. Runs on the placeholder form produced by
|
|
54
|
+
* escapeNestedScriptClose, so an inner `</script>` cannot end the match early.
|
|
55
|
+
*
|
|
56
|
+
* Exported for tests.
|
|
57
|
+
*/
|
|
58
|
+
export function escapeScriptBodiesForJsxTemplateLiteral(html: string): string {
|
|
59
|
+
return html.replace(
|
|
60
|
+
/(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi,
|
|
61
|
+
(_fullMatch: string, openTag: string, body: string, closeTag: string) =>
|
|
62
|
+
openTag +
|
|
63
|
+
body
|
|
64
|
+
.replace(/\\*`/g, (match: string) => {
|
|
65
|
+
const backslashCount = match.length - 1
|
|
66
|
+
return backslashCount % 2 === 0 ? match.slice(0, backslashCount) + '\\`' : match
|
|
67
|
+
})
|
|
68
|
+
.replace(/\\*\$\{/g, (match: string) => {
|
|
69
|
+
const backslashCount = match.length - 2
|
|
70
|
+
return backslashCount % 2 === 0 ? match.slice(0, backslashCount) + '\\${' : match
|
|
71
|
+
}) +
|
|
72
|
+
closeTag
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
10
76
|
const NODE_MAPPER: Record<
|
|
11
77
|
SUPPORTED_PROJECT_TYPES,
|
|
12
78
|
Promise<(content: unknown, options: unknown) => string>
|
|
13
79
|
> = {
|
|
14
|
-
html: import('hast-util-to-html').then((mod) => mod.toHtml),
|
|
15
|
-
jsx: import('hast-util-to-jsx-inline-script').then((mod) => mod.default),
|
|
16
80
|
'teleport-project-html': import('hast-util-to-html').then((mod) => mod.toHtml),
|
|
81
|
+
'teleport-project-angular': import('hast-util-to-html').then((mod) => mod.toHtml),
|
|
82
|
+
'teleport-project-vue': import('hast-util-to-html').then((mod) => mod.toHtml),
|
|
83
|
+
'teleport-project-nuxt': import('hast-util-to-html').then((mod) => mod.toHtml),
|
|
17
84
|
'teleport-project-react': import('hast-util-to-jsx-inline-script').then((mod) => mod.default),
|
|
18
85
|
'teleport-project-next': import('hast-util-to-jsx-inline-script').then((mod) => mod.default),
|
|
19
86
|
}
|
|
20
87
|
|
|
21
88
|
const COMPONENT_CHUNK_NAMES: Record<SUPPORTED_PROJECT_TYPES, string> = {
|
|
22
|
-
html: 'html-chunk',
|
|
23
|
-
jsx: 'jsx-component',
|
|
24
89
|
'teleport-project-html': 'html-chunk',
|
|
90
|
+
'teleport-project-angular': 'template-chunk',
|
|
91
|
+
'teleport-project-vue': 'template-chunk',
|
|
92
|
+
'teleport-project-nuxt': 'template-chunk',
|
|
25
93
|
'teleport-project-next': 'jsx-component',
|
|
26
94
|
'teleport-project-react': 'jsx-component',
|
|
27
95
|
}
|
|
@@ -51,12 +119,50 @@ export const createParseEmbedPlugin: ComponentPluginFactory<ParseEmbedPluginConf
|
|
|
51
119
|
(elementType === 'dangerous-html' || element.dependency?.path === 'dangerous-html') &&
|
|
52
120
|
attrs?.html
|
|
53
121
|
) {
|
|
54
|
-
|
|
122
|
+
if (attrs.html.type === 'raw' && (attrs.html as any).dynamic) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let rawHtmlContent = element.attrs.html.content as string
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
projectType === 'teleport-project-html' ||
|
|
130
|
+
projectType === 'teleport-project-angular' ||
|
|
131
|
+
projectType === 'teleport-project-vue' ||
|
|
132
|
+
projectType === 'teleport-project-nuxt'
|
|
133
|
+
) {
|
|
134
|
+
// Strip pre-escaped backticks (\` → `) for HTML/template frameworks.
|
|
135
|
+
// In these contexts backticks are just regular characters and the
|
|
136
|
+
// escaping would be a JavaScript syntax error inside <script> tags.
|
|
137
|
+
rawHtmlContent = rawHtmlContent.replace(/\\`/g, '`')
|
|
138
|
+
}
|
|
139
|
+
// Escape nested </script> tags so hast-util-from-html doesn't truncate
|
|
140
|
+
// script content at an inner </script> (e.g. one inside a template literal).
|
|
141
|
+
rawHtmlContent = escapeNestedScriptClose(rawHtmlContent)
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
projectType === 'teleport-project-react' ||
|
|
145
|
+
projectType === 'teleport-project-next'
|
|
146
|
+
) {
|
|
147
|
+
// For React/Next the fork wraps script bodies in an outer {`…`}
|
|
148
|
+
// template literal — make them safe for it ourselves. Parity-aware:
|
|
149
|
+
// already-escaped input (the GUI mapper pre-escapes today) passes
|
|
150
|
+
// through unchanged, raw input gets escaped, so this is safe on both
|
|
151
|
+
// sides of the GUI dropping its pre-escape step.
|
|
152
|
+
rawHtmlContent = escapeScriptBodiesForJsxTemplateLiteral(rawHtmlContent)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hastNodes = fromHtml(rawHtmlContent, {
|
|
55
156
|
fragment: true,
|
|
56
157
|
})
|
|
57
158
|
const content = hastToJsxOrHtml(hastNodes, { wrapper: 'fragment' })
|
|
58
159
|
|
|
59
|
-
if (
|
|
160
|
+
if (
|
|
161
|
+
projectType === 'teleport-project-html' ||
|
|
162
|
+
projectType === 'teleport-project-angular' ||
|
|
163
|
+
projectType === 'teleport-project-vue' ||
|
|
164
|
+
projectType === 'teleport-project-nuxt'
|
|
165
|
+
) {
|
|
60
166
|
const node = compontnChunk.meta.nodesLookup[key] as HastNode
|
|
61
167
|
if (!node) {
|
|
62
168
|
return
|
|
@@ -67,7 +173,10 @@ export const createParseEmbedPlugin: ComponentPluginFactory<ParseEmbedPluginConf
|
|
|
67
173
|
|
|
68
174
|
Object.assign(node, {
|
|
69
175
|
type: 'text',
|
|
70
|
-
|
|
176
|
+
// Restore nested </script> with <\/script> — the backslash prevents
|
|
177
|
+
// the browser's HTML parser from closing the script tag prematurely,
|
|
178
|
+
// and \/ evaluates to / in JavaScript at runtime.
|
|
179
|
+
value: content.replace(new RegExp(SCRIPT_CLOSE_PH, 'g'), '<\\/script>'),
|
|
71
180
|
})
|
|
72
181
|
|
|
73
182
|
delete node.children
|
|
@@ -83,7 +192,13 @@ export const createParseEmbedPlugin: ComponentPluginFactory<ParseEmbedPluginConf
|
|
|
83
192
|
node.openingElement.attributes = []
|
|
84
193
|
node.children.push({
|
|
85
194
|
type: 'JSXText' as const,
|
|
86
|
-
|
|
195
|
+
// Double-escape </script> for the outer template literal {`…`}:
|
|
196
|
+
// file: <\\/script>
|
|
197
|
+
// outer `` eval: <\/script> (\\→\, HTML parser won't close)
|
|
198
|
+
// inner `` eval: </script> (\/→/, correct runtime value)
|
|
199
|
+
value: content
|
|
200
|
+
.replace(new RegExp(SCRIPT_CLOSE_PH, 'g'), '<\\\\/script>')
|
|
201
|
+
.replace(/<\\\/script/g, '<\\\\/script'),
|
|
87
202
|
} as JSXText)
|
|
88
203
|
}
|
|
89
204
|
}
|
package/src/utils.ts
CHANGED
|
@@ -4,8 +4,9 @@ export type SUPPORTED_PROJECT_TYPES =
|
|
|
4
4
|
| 'teleport-project-html'
|
|
5
5
|
| 'teleport-project-react'
|
|
6
6
|
| 'teleport-project-next'
|
|
7
|
-
| '
|
|
8
|
-
| '
|
|
7
|
+
| 'teleport-project-angular'
|
|
8
|
+
| 'teleport-project-vue'
|
|
9
|
+
| 'teleport-project-nuxt'
|
|
9
10
|
|
|
10
11
|
export const JS_EXECUTION_DEPENDENCIES: Record<string, UIDLExternalDependency> = {
|
|
11
12
|
'teleport-project-react': {
|