lambder 7.2.5 → 8.0.2

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.
Files changed (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +161 -69
  41. package/dist/core/Lambder.js +370 -226
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +3 -1
  93. package/dist/mock.js +5 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -0,0 +1,652 @@
1
+ /*
2
+ * Where a value inserted into HTML lands, and what HTML escaping cannot
3
+ * protect there. The html`...` tagged template (LambderHtml) and the file
4
+ * template engine (core/LambderTemplatingEngine) both place their values
5
+ * with it, so the two refuse the same positions and check a URL value by the
6
+ * same rule. Pure string code: it ships to the browser with html`...`.
7
+ */
8
+ /**
9
+ * The elements whose content the browser reads as text up to their own end
10
+ * tag, with no tags or comments inside: script data, RAWTEXT (style, xmp,
11
+ * iframe, noembed, noframes) and RCDATA (title, textarea). <noscript> is read
12
+ * as markup, the way a parser with scripting off (DOMParser, a mail client)
13
+ * reads it.
14
+ */
15
+ const TEXT_CONTENT_ELEMENTS = new Set(["script", "style", "xmp", "iframe", "noembed", "noframes", "title", "textarea"]);
16
+ /**
17
+ * Of those, the ones whose content is another language, where HTML escaping
18
+ * is the wrong grammar: a value inside a <script> or <style> is refused. In
19
+ * the rest an escaped value is text, since its "<" is escaped and cannot end
20
+ * the element, until their content holds a tag (see foreignPartedBy).
21
+ */
22
+ export const RAW_TEXT_ELEMENTS = new Set(["script", "style"]);
23
+ /** What HTML reads as whitespace between a tag's parts: tab, line feed, form feed, carriage return and space, and no other \s. */
24
+ const HTML_WHITESPACE = /[\t\n\f\r ]/;
25
+ const ASCII_LETTER = /[A-Za-z]/;
26
+ /** A run of tag name characters, up to the whitespace, "/" or ">" that ends the name. */
27
+ const TAG_NAME_RUN = /[^\t\n\f\r />]*/y;
28
+ /** A run of attribute name characters, up to the whitespace, "/", "=" or ">" that ends the name. */
29
+ const ATTRIBUTE_NAME_RUN = /[^\t\n\f\r />=]*/y;
30
+ /** What changes the state of a script's escaped content: a "-" or a "<". */
31
+ const SCRIPT_ESCAPED_STOP = /[-<]/g;
32
+ /** What follows "<!" to open a CDATA section, which only inline SVG and MathML read as one. */
33
+ const CDATA_OPENER = "[CDATA[";
34
+ // The tokenizer's comment states, as bits, so a comment can be in several at
35
+ // once right after a value (see skipValue). Between them they end a comment
36
+ // where the browser does: at "-->" and "--!>", and at "<!-->" and "<!--->"
37
+ // right after it opens. The comment less-than sign states the standard adds
38
+ // for a "<!--" inside a comment end it at the same place as these.
39
+ const COMMENT_START = 1;
40
+ const COMMENT_START_DASH = 2;
41
+ const COMMENT_TEXT = 4;
42
+ const COMMENT_END_DASH = 8;
43
+ const COMMENT_END = 16;
44
+ const COMMENT_END_BANG = 32;
45
+ /** What nextCommentState answers for a character that ends the comment. */
46
+ const COMMENT_CLOSED = 0;
47
+ /** The comment state `state` moves to on `character`, or COMMENT_CLOSED when the character ends the comment. */
48
+ const nextCommentState = (state, character) => {
49
+ if (character === ">")
50
+ return state === COMMENT_TEXT || state === COMMENT_END_DASH ? COMMENT_TEXT : COMMENT_CLOSED;
51
+ if (character === "-") {
52
+ if (state === COMMENT_START)
53
+ return COMMENT_START_DASH;
54
+ return state === COMMENT_TEXT || state === COMMENT_END_BANG ? COMMENT_END_DASH : COMMENT_END;
55
+ }
56
+ return character === "!" && state === COMMENT_END ? COMMENT_END_BANG : COMMENT_TEXT;
57
+ };
58
+ /**
59
+ * Says where the end of the HTML read so far sits, with the part of the HTML
60
+ * standard's tokenizer that decides it (ReaderState): an `=` or a `>` inside
61
+ * a quoted value, or in text and comments, counts for nothing, a comment ends
62
+ * where the browser ends it, and the content of a <script>, <style>, <title>
63
+ * and the other TEXT_CONTENT_ELEMENTS is skipped up to its own end tag.
64
+ *
65
+ * A template's static pieces are read one after another, and a piece may end
66
+ * anywhere, in the middle of "<!-" included: the reader asks where each value
67
+ * between them lands (point) and moves past it (skipValue). The values
68
+ * themselves are never read, since an escaped one holds no "<", ">" or
69
+ * quote, so the position a value lands in is a property of the template. A
70
+ * reading can be copied and joined with another reading of the same
71
+ * template, which is how the template engine reads the branches of an
72
+ * if/else.
73
+ */
74
+ export class HtmlPositionReader {
75
+ state = "data";
76
+ /** The name of the tag being read, or of the element whose content is, lowercased. */
77
+ tagName = "";
78
+ /** The tag being read is an end tag, whose ">" never opens text content. */
79
+ endTag = false;
80
+ /** The attribute whose name or value is being read; between attributes, the one an "=" gives a value to ("" for none). */
81
+ attributeName = "";
82
+ /** The quote character the value being read opened with. */
83
+ quote = "";
84
+ /** Between attributes: this reading joins readings that give an "=" to different attributes. */
85
+ equalsAmbiguous = false;
86
+ /** Inside a comment: the COMMENT_ states it may be in, as bits. */
87
+ commentStates = 0;
88
+ /** Inside a <script>: 0 in script data, 1 escaped (after "<!--"), 2 double escaped (after "<!--" and "<script"). */
89
+ scriptLevel = 0;
90
+ /** Inside text content: the end tag name read so far, or the script tag name that double escapes a script or undoes that. */
91
+ nameBuffer = "";
92
+ /**
93
+ * What has left inline SVG and MathML reading the HTML read so far apart
94
+ * from plain HTML, or "" while the two read it alike. Inside them the
95
+ * content of the TEXT_CONTENT_ELEMENTS is markup, and "<![CDATA[" runs to
96
+ * "]]>" where plain HTML ends a bogus comment at the first ">". Once
97
+ * text-only content holds a tag, or a CDATA section may end elsewhere
98
+ * than plain HTML's comment, one reading can sit inside a tag where the
99
+ * other sits in text, past the element's end tag too, so every value
100
+ * from there on is a rawText point. A <script> or <style> is left out: a
101
+ * value inside one is refused anyway, and a "<" in them (`a<b`) is common
102
+ * enough that parting there would refuse every value after most inline
103
+ * scripts.
104
+ */
105
+ foreignPartedBy = "";
106
+ /** The label an undecidable reading is blamed on: the last value placed in a comment, or the join between attributes. */
107
+ blame = "";
108
+ undecidableReason = null;
109
+ /** Read `html` on from where the last read stopped. */
110
+ read(html) {
111
+ let index = 0;
112
+ while (index < html.length && this.state !== "undecidable") {
113
+ const character = html[index];
114
+ switch (this.state) {
115
+ case "data": {
116
+ const open = html.indexOf("<", index);
117
+ if (open === -1)
118
+ return;
119
+ this.state = "tagOpen";
120
+ index = open + 1;
121
+ break;
122
+ }
123
+ case "tagOpen":
124
+ if (character === "!" || character === "/") {
125
+ this.state = character === "!" ? "markupOpen" : "endTagOpen";
126
+ index += 1;
127
+ }
128
+ else if (ASCII_LETTER.test(character))
129
+ this.openTag(false);
130
+ else
131
+ this.state = character === "?" ? "bogusComment" : "data";
132
+ break;
133
+ case "endTagOpen":
134
+ if (ASCII_LETTER.test(character))
135
+ this.openTag(true);
136
+ else if (character === ">") {
137
+ this.state = "data";
138
+ index += 1;
139
+ }
140
+ else
141
+ this.state = "bogusComment";
142
+ break;
143
+ case "markupOpen":
144
+ case "markupOpenDash":
145
+ if (character !== "-") {
146
+ // A CDATA section, or a "<![" this piece ends in,
147
+ // reads alike both ways only when the first ">" is
148
+ // the one of "]]>" and this piece holds it.
149
+ if (this.state === "markupOpen" && CDATA_OPENER.startsWith(html.slice(index, index + CDATA_OPENER.length))) {
150
+ const close = html.indexOf(">", index);
151
+ if (close === -1 || !html.startsWith("]]", close - 2))
152
+ this.foreignPartedBy = "![CDATA[ ... ]]";
153
+ }
154
+ this.state = "bogusComment";
155
+ break;
156
+ }
157
+ index += 1;
158
+ if (this.state === "markupOpen")
159
+ this.state = "markupOpenDash";
160
+ else {
161
+ this.state = "comment";
162
+ this.commentStates = COMMENT_START;
163
+ }
164
+ break;
165
+ case "tagName":
166
+ if (HTML_WHITESPACE.test(character) || character === "/") {
167
+ this.enterBetweenAttributes("");
168
+ index += 1;
169
+ }
170
+ else if (character === ">") {
171
+ this.closeTag();
172
+ index += 1;
173
+ }
174
+ else {
175
+ TAG_NAME_RUN.lastIndex = index;
176
+ const name = TAG_NAME_RUN.exec(html)[0];
177
+ this.tagName += name.toLowerCase();
178
+ index += name.length;
179
+ }
180
+ break;
181
+ case "betweenAttributes":
182
+ if (HTML_WHITESPACE.test(character)) {
183
+ index += 1;
184
+ break;
185
+ }
186
+ if (character === "=" && this.equalsAmbiguous) {
187
+ this.loseTrack("attributeEquals");
188
+ break;
189
+ }
190
+ this.equalsAmbiguous = false;
191
+ if (character === "/") {
192
+ this.attributeName = "";
193
+ index += 1;
194
+ }
195
+ else if (character === ">") {
196
+ this.closeTag();
197
+ index += 1;
198
+ }
199
+ else if (character === "=") {
200
+ // With no name waiting for a value, an "=" starts
201
+ // the next attribute's name.
202
+ if (this.attributeName === "") {
203
+ this.state = "attributeName";
204
+ this.attributeName = "=";
205
+ }
206
+ else
207
+ this.state = "beforeValue";
208
+ index += 1;
209
+ }
210
+ else {
211
+ this.state = "attributeName";
212
+ this.attributeName = "";
213
+ }
214
+ break;
215
+ case "attributeName":
216
+ if (HTML_WHITESPACE.test(character)) {
217
+ this.enterBetweenAttributes(this.attributeName);
218
+ index += 1;
219
+ }
220
+ else if (character === "/") {
221
+ this.enterBetweenAttributes("");
222
+ index += 1;
223
+ }
224
+ else if (character === ">") {
225
+ this.closeTag();
226
+ index += 1;
227
+ }
228
+ else if (character === "=") {
229
+ this.state = "beforeValue";
230
+ index += 1;
231
+ }
232
+ else {
233
+ ATTRIBUTE_NAME_RUN.lastIndex = index;
234
+ const name = ATTRIBUTE_NAME_RUN.exec(html)[0];
235
+ this.attributeName += name.toLowerCase();
236
+ index += name.length;
237
+ }
238
+ break;
239
+ case "beforeValue":
240
+ if (character === '"' || character === "'") {
241
+ this.state = "quotedValue";
242
+ this.quote = character;
243
+ }
244
+ else if (character === ">")
245
+ this.closeTag();
246
+ else if (!HTML_WHITESPACE.test(character))
247
+ this.state = "unquotedValue";
248
+ index += 1;
249
+ break;
250
+ case "unquotedValue":
251
+ if (HTML_WHITESPACE.test(character))
252
+ this.enterBetweenAttributes("");
253
+ else if (character === ">")
254
+ this.closeTag();
255
+ index += 1;
256
+ break;
257
+ case "quotedValue": {
258
+ const close = html.indexOf(this.quote, index);
259
+ if (close === -1)
260
+ return;
261
+ this.enterBetweenAttributes("");
262
+ index = close + 1;
263
+ break;
264
+ }
265
+ case "bogusComment": {
266
+ const close = html.indexOf(">", index);
267
+ if (close === -1)
268
+ return;
269
+ this.state = "data";
270
+ index = close + 1;
271
+ break;
272
+ }
273
+ case "comment": {
274
+ // Plain comment text changes nothing up to its next "-".
275
+ if (this.commentStates === COMMENT_TEXT) {
276
+ const dash = html.indexOf("-", index);
277
+ if (dash === -1)
278
+ return;
279
+ index = dash;
280
+ }
281
+ let nextStates = 0;
282
+ let closes = false;
283
+ for (let commentState = COMMENT_START; commentState <= COMMENT_END_BANG; commentState <<= 1) {
284
+ if (!(this.commentStates & commentState))
285
+ continue;
286
+ const next = nextCommentState(commentState, html[index]);
287
+ if (next === COMMENT_CLOSED)
288
+ closes = true;
289
+ else
290
+ nextStates |= next;
291
+ }
292
+ index += 1;
293
+ if (closes && nextStates !== 0)
294
+ this.loseTrack("commentValue");
295
+ else if (closes)
296
+ this.state = "data";
297
+ else
298
+ this.commentStates = nextStates;
299
+ break;
300
+ }
301
+ case "rawText": {
302
+ // A script's escaped content changes state on a "-" as
303
+ // well as on a "<".
304
+ let stop = -1;
305
+ if (this.scriptLevel === 0)
306
+ stop = html.indexOf("<", index);
307
+ else {
308
+ SCRIPT_ESCAPED_STOP.lastIndex = index;
309
+ stop = SCRIPT_ESCAPED_STOP.exec(html)?.index ?? -1;
310
+ }
311
+ if (stop === -1)
312
+ return;
313
+ this.state = html[stop] === "-" ? "rawTextDash" : "rawTextLessThan";
314
+ index = stop + 1;
315
+ break;
316
+ }
317
+ case "rawTextDash":
318
+ case "rawTextDashDash":
319
+ if (character === "<")
320
+ this.state = "rawTextLessThan";
321
+ else if (character === "-")
322
+ this.state = "rawTextDashDash";
323
+ else {
324
+ // "-->" ends a script's escaped content.
325
+ if (character === ">" && this.state === "rawTextDashDash")
326
+ this.scriptLevel = 0;
327
+ this.state = "rawText";
328
+ }
329
+ index += 1;
330
+ break;
331
+ case "rawTextLessThan":
332
+ if (character === "/") {
333
+ this.state = this.scriptLevel === 2 ? "scriptDoubleEscapeEnd" : "rawTextEndTagOpen";
334
+ this.nameBuffer = "";
335
+ index += 1;
336
+ }
337
+ else if (character === "!" && this.tagName === "script" && this.scriptLevel === 0) {
338
+ this.state = "scriptEscapeStart";
339
+ index += 1;
340
+ }
341
+ else if (ASCII_LETTER.test(character) && this.scriptLevel === 1) {
342
+ this.state = "scriptDoubleEscapeStart";
343
+ this.nameBuffer = "";
344
+ }
345
+ else {
346
+ if (ASCII_LETTER.test(character) || character === "!" || character === "?")
347
+ this.noteMarkupInText();
348
+ this.state = "rawText";
349
+ }
350
+ break;
351
+ case "rawTextEndTagOpen":
352
+ if (ASCII_LETTER.test(character))
353
+ this.state = "rawTextEndTagName";
354
+ else {
355
+ this.noteMarkupInText();
356
+ this.state = "rawText";
357
+ }
358
+ break;
359
+ case "rawTextEndTagName":
360
+ if (ASCII_LETTER.test(character)) {
361
+ this.nameBuffer += character.toLowerCase();
362
+ index += 1;
363
+ }
364
+ else if (this.nameBuffer === this.tagName && (HTML_WHITESPACE.test(character) || character === "/" || character === ">")) {
365
+ // The element's own end tag, whose attributes are
366
+ // read like any tag's.
367
+ this.endTag = true;
368
+ if (character === ">")
369
+ this.closeTag();
370
+ else
371
+ this.enterBetweenAttributes("");
372
+ index += 1;
373
+ }
374
+ else {
375
+ this.noteMarkupInText();
376
+ this.state = "rawText";
377
+ }
378
+ break;
379
+ case "scriptEscapeStart":
380
+ case "scriptEscapeStartDash":
381
+ if (character !== "-") {
382
+ this.state = "rawText";
383
+ break;
384
+ }
385
+ index += 1;
386
+ if (this.state === "scriptEscapeStart")
387
+ this.state = "scriptEscapeStartDash";
388
+ else {
389
+ this.state = "rawTextDashDash";
390
+ this.scriptLevel = 1;
391
+ }
392
+ break;
393
+ case "scriptDoubleEscapeStart":
394
+ case "scriptDoubleEscapeEnd":
395
+ if (ASCII_LETTER.test(character)) {
396
+ this.nameBuffer += character.toLowerCase();
397
+ index += 1;
398
+ break;
399
+ }
400
+ if (HTML_WHITESPACE.test(character) || character === "/" || character === ">") {
401
+ // A "<script" in escaped script content double
402
+ // escapes it, so its own "</script>" does not end
403
+ // the element; that "</script>" only undoes it.
404
+ if (this.nameBuffer === "script")
405
+ this.scriptLevel = this.state === "scriptDoubleEscapeStart" ? 2 : 1;
406
+ index += 1;
407
+ }
408
+ this.state = "rawText";
409
+ break;
410
+ }
411
+ }
412
+ }
413
+ /** Where a value inserted at the end of the HTML read so far lands. */
414
+ point() {
415
+ const point = { position: "tag", attributeName: "", quote: "", tagName: "" };
416
+ if (this.foreignPartedBy) {
417
+ point.position = "rawText";
418
+ point.tagName = this.foreignPartedBy;
419
+ return point;
420
+ }
421
+ switch (this.state) {
422
+ case "data":
423
+ point.position = "text";
424
+ break;
425
+ case "bogusComment":
426
+ case "comment":
427
+ point.position = "comment";
428
+ break;
429
+ case "beforeValue":
430
+ case "unquotedValue":
431
+ case "quotedValue":
432
+ point.position = this.state === "beforeValue" ? "afterEquals" : this.state === "unquotedValue" ? "unquoted" : "quoted";
433
+ point.attributeName = this.attributeName;
434
+ if (this.state === "quotedValue")
435
+ point.quote = this.quote;
436
+ break;
437
+ case "rawText":
438
+ case "rawTextDash":
439
+ case "rawTextDashDash":
440
+ case "rawTextLessThan":
441
+ case "rawTextEndTagOpen":
442
+ case "rawTextEndTagName":
443
+ case "scriptEscapeStart":
444
+ case "scriptEscapeStartDash":
445
+ case "scriptDoubleEscapeStart":
446
+ case "scriptDoubleEscapeEnd":
447
+ // Inside other text content a value is text, unless it
448
+ // follows a "<" or an end tag's start, which it could finish.
449
+ if (RAW_TEXT_ELEMENTS.has(this.tagName)) {
450
+ point.position = "rawText";
451
+ point.tagName = this.tagName;
452
+ }
453
+ else if (this.state === "rawText")
454
+ point.position = "text";
455
+ break;
456
+ default:
457
+ // A tag, or a "<", "</", "<!" or "<!-" a value would finish:
458
+ // where a value goes on the markup the template started.
459
+ break;
460
+ }
461
+ return point;
462
+ }
463
+ /**
464
+ * Move past a value inserted here. An escaped value holds no "<", ">" or
465
+ * quote, so only a comment reads on differently after it: the value may
466
+ * end in "-", "--" or "--!", and the text after it may then end the
467
+ * comment on a ">" that ends it after no other value. The reader keeps
468
+ * every comment state such an ending leaves, and loses track (blaming
469
+ * `label`) when the text after them ends the comment in some and not in
470
+ * others.
471
+ */
472
+ skipValue(label = "") {
473
+ if (this.state !== "comment")
474
+ return;
475
+ this.commentStates |= COMMENT_TEXT | COMMENT_END_DASH | COMMENT_END | COMMENT_END_BANG
476
+ | (this.commentStates & COMMENT_START ? COMMENT_START_DASH : 0);
477
+ this.blame = label;
478
+ }
479
+ /** An independent copy of this reading, to read one branch of a template on from here. */
480
+ copy() {
481
+ return Object.assign(new HtmlPositionReader(), this);
482
+ }
483
+ /**
484
+ * Take in `other`, a reading of the same template along another branch,
485
+ * when both sit at the same position, and say whether they did: from
486
+ * then on this reading reads for both. Between attributes, readings
487
+ * whose last attribute names differ count as the same position, so a
488
+ * conditional boolean attribute (`<input <!--if:x-->checked<!--/if:x--> name="a">`)
489
+ * reads on as one reading. Only an "=" next reads them apart, and it
490
+ * makes the joined reading lose track, blaming `label`.
491
+ */
492
+ joinWith(other, label = "") {
493
+ if (this.positionKey() !== other.positionKey() || this.foreignPartedBy !== other.foreignPartedBy)
494
+ return false;
495
+ if (this.state === "betweenAttributes" && !this.equalsAmbiguous) {
496
+ if (other.equalsAmbiguous) {
497
+ this.equalsAmbiguous = true;
498
+ this.blame = other.blame;
499
+ }
500
+ else if (other.attributeName !== this.attributeName) {
501
+ this.equalsAmbiguous = true;
502
+ this.blame = label;
503
+ }
504
+ }
505
+ return true;
506
+ }
507
+ /** Why the reader has lost track of where the HTML read so far sits, or null while it has not. A caller refuses the template then. */
508
+ get undecidable() {
509
+ return this.undecidableReason ? { reason: this.undecidableReason, label: this.blame } : null;
510
+ }
511
+ /**
512
+ * The HTML read so far ends in plain text, where complete markup ends:
513
+ * outside any tag, comment and text content, and read alike by inline
514
+ * SVG and MathML. A fragment inserted into another template has to end
515
+ * here, since the template around it places its values without reading it.
516
+ */
517
+ get inPlainText() {
518
+ return this.state === "data" && !this.foreignPartedBy;
519
+ }
520
+ /** The state and whichever fields it reads on with, as one string: two readings with the same key read any text the same way. */
521
+ positionKey() {
522
+ switch (this.state) {
523
+ case "comment":
524
+ return `comment ${this.commentStates}`;
525
+ case "tagName":
526
+ return `tagName ${this.endTag} ${this.tagName}`;
527
+ case "betweenAttributes":
528
+ case "attributeName":
529
+ case "beforeValue":
530
+ case "unquotedValue":
531
+ case "quotedValue": {
532
+ // Past its name, a tag's name matters only where its ">"
533
+ // opens text content.
534
+ const opens = this.endTag || !TEXT_CONTENT_ELEMENTS.has(this.tagName) ? "" : this.tagName;
535
+ return `${this.state} ${opens} ${this.state === "betweenAttributes" ? "" : this.attributeName} ${this.state === "quotedValue" ? this.quote : ""}`;
536
+ }
537
+ case "rawText":
538
+ case "rawTextDash":
539
+ case "rawTextDashDash":
540
+ case "rawTextLessThan":
541
+ case "rawTextEndTagOpen":
542
+ case "scriptEscapeStart":
543
+ case "scriptEscapeStartDash":
544
+ return `${this.state} ${this.tagName} ${this.scriptLevel}`;
545
+ case "rawTextEndTagName":
546
+ case "scriptDoubleEscapeStart":
547
+ case "scriptDoubleEscapeEnd":
548
+ return `${this.state} ${this.tagName} ${this.scriptLevel} ${this.nameBuffer}`;
549
+ default:
550
+ return this.state;
551
+ }
552
+ }
553
+ openTag(endTag) {
554
+ this.state = "tagName";
555
+ this.tagName = "";
556
+ this.endTag = endTag;
557
+ }
558
+ enterBetweenAttributes(attributeName) {
559
+ this.state = "betweenAttributes";
560
+ this.attributeName = attributeName;
561
+ this.equalsAmbiguous = false;
562
+ }
563
+ /**
564
+ * A tag's ">" leads into its content: text content for the
565
+ * TEXT_CONTENT_ELEMENTS even when written `<script />`, which HTML reads
566
+ * as an open element, and data after an end tag.
567
+ */
568
+ closeTag() {
569
+ this.state = !this.endTag && TEXT_CONTENT_ELEMENTS.has(this.tagName) ? "rawText" : "data";
570
+ this.scriptLevel = 0;
571
+ }
572
+ /**
573
+ * A "<" in text content starts a tag, a comment or a bogus comment in
574
+ * markup, and it is not the element's own end tag: inline SVG and MathML
575
+ * read it as markup (see foreignPartedBy).
576
+ */
577
+ noteMarkupInText() {
578
+ if (!RAW_TEXT_ELEMENTS.has(this.tagName))
579
+ this.foreignPartedBy = this.tagName;
580
+ }
581
+ loseTrack(reason) {
582
+ this.state = "undecidable";
583
+ this.undecidableReason = reason;
584
+ }
585
+ }
586
+ /**
587
+ * The language the browser reads an attribute's value in once it has
588
+ * decoded the HTML escapes, for the attributes whose value is not text: an
589
+ * event handler (any `on` attribute) runs as JavaScript, `style` is read as
590
+ * CSS and `srcdoc` as a whole document. Escaping keeps a value inside the
591
+ * quotes and nothing more there: `onclick="go('${x}')"` runs
592
+ * `');alert(1);//`, since `&#39;` is a quote again by the time the script
593
+ * sees it.
594
+ */
595
+ export const valueLanguageOf = (attributeName) => {
596
+ if (attributeName.startsWith("on"))
597
+ return "JavaScript";
598
+ if (attributeName === "style")
599
+ return "CSS";
600
+ if (attributeName === "srcdoc")
601
+ return "an HTML document";
602
+ return null;
603
+ };
604
+ /**
605
+ * The attributes whose value is one URL. In several a `javascript:` value
606
+ * runs script (a link, a form's action, a frame's src), and in the rest a
607
+ * scheme an inserted value picked is still one the page never meant to point
608
+ * at. A quoted value of one of them holding an inserted value is rendered
609
+ * whole and checked (safeUrlValue). The list-valued ones (srcset, ping) are
610
+ * left out: their entries never run.
611
+ */
612
+ export const URL_ATTRIBUTE_NAMES = new Set([
613
+ "href", "src", "action", "formaction", "xlink:href", "poster", "cite",
614
+ "data", "background", "codebase", "longdesc", "manifest",
615
+ ]);
616
+ /** The schemes an inserted value may give a URL attribute: the ones a link or a resource uses, and none that runs code or carries its own document. */
617
+ const SAFE_URL_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
618
+ /** What a URL value renders as when an inserted value gave it any other scheme: a URL that goes nowhere and runs nothing. */
619
+ const NEUTRALIZED_URL_VALUE = "about:invalid";
620
+ /**
621
+ * A rendered URL value `url`, or NEUTRALIZED_URL_VALUE when an inserted value
622
+ * may have given it a scheme outside SAFE_URL_SCHEMES. `firstValueIndex` is
623
+ * where in `url` the first inserted value landed, -1 when none did. Once the
624
+ * template's own text before that point holds a `:`, `/`, `?` or `#`, the
625
+ * template fixed the scheme (`/users/${id}`, `sms:${number}`) and the value
626
+ * stands as rendered. Otherwise the whole value is read for its scheme the
627
+ * way a browser reads it: leading and trailing control characters and spaces
628
+ * dropped, tabs and line breaks removed wherever they are, letters in any
629
+ * case. A value with no scheme is relative and stands. A character reference
630
+ * (`&`) where the scheme or its colon would be is refused rather than
631
+ * decoded: `&colon;` and `&#58;` are colons to the browser. Whatever kind of
632
+ * value was inserted, html`...` and raw() included, the rule is the same.
633
+ */
634
+ export const safeUrlValue = (url, firstValueIndex) => {
635
+ if (firstValueIndex === -1 || /^[^&:/?#]*[:/?#]/.test(url.slice(0, firstValueIndex)))
636
+ return url;
637
+ // A URL parser drops C0 controls and spaces (U+0000 to U+0020) at both ends.
638
+ let first = 0;
639
+ let last = url.length;
640
+ while (first < last && url.charCodeAt(first) <= 0x20)
641
+ first += 1;
642
+ while (last > first && url.charCodeAt(last - 1) <= 0x20)
643
+ last -= 1;
644
+ const urlText = url.slice(first, last).replace(/[\t\n\r]/g, "");
645
+ const scheme = /^[A-Za-z][A-Za-z0-9+.-]*/.exec(urlText)?.[0] ?? "";
646
+ const afterScheme = urlText.charAt(scheme.length);
647
+ if (afterScheme === "&")
648
+ return NEUTRALIZED_URL_VALUE;
649
+ if (afterScheme === ":" && scheme !== "" && !SAFE_URL_SCHEMES.has(scheme.toLowerCase()))
650
+ return NEUTRALIZED_URL_VALUE;
651
+ return url;
652
+ };