lambder 7.3.1 → 8.1.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/CHANGELOG.md +1047 -3
- package/README.md +46 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/ContractTypePrinter.d.ts +85 -0
- package/dist/build/ContractTypePrinter.js +402 -0
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/moduleLocation.d.ts +11 -0
- package/dist/build/moduleLocation.js +6 -0
- package/dist/build/writeApiContract.d.ts +78 -0
- package/dist/build/writeApiContract.js +302 -0
- package/dist/build/writeApiSignatures.d.ts +114 -0
- package/dist/build/writeApiSignatures.js +217 -0
- package/dist/build/writeFileAtomically.d.ts +8 -0
- package/dist/build/writeFileAtomically.js +22 -0
- package/dist/build.d.ts +14 -0
- package/dist/build.js +11 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/LambderUploadRunner.d.ts +96 -0
- package/dist/client/LambderUploadRunner.js +234 -0
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +9 -3
- package/dist/client.js +6 -1
- package/dist/core/Lambder.d.ts +143 -79
- package/dist/core/Lambder.js +350 -231
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +21 -7
- package/dist/core/LambderFiles.js +62 -34
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +25 -8
- package/dist/index.js +13 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +43 -33
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
- package/dist/mock/lambderMockUploadMswHandler.js +28 -0
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +6 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
- package/dist/shared/contracts/LambderUploadBucket.js +74 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
- package/dist/shared/util/LambderContentDisposition.js +13 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTextDigest.d.ts +7 -5
- package/dist/shared/util/LambderTextDigest.js +11 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +98 -53
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
- package/dist/shared/wire/LambderApiRefusal.js +42 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
- package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
- package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
- package/dist/shared/wire/LambderUploadRefusal.js +18 -0
- package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
- package/dist/shared/wire/LambderUploadSchemas.js +30 -0
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +80 -38
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
- package/dist/stores/LambderMemoryUploadBucket.js +219 -0
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
- package/dist/stores/LambderS3UploadBucket.js +144 -0
- package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
- package/dist/stores/LambderSdkInstallHint.js +14 -0
- package/dist/testing/LambderTestApp.d.ts +23 -25
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +3 -0
- package/dist/testing.js +2 -0
- package/package.json +26 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- 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 `'` 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: `:` and `:` 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
|
+
};
|