@johnhenry/letterpress 0.0.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +57 -1
- package/CHANGELOG.md +127 -0
- package/README.md +87 -3
- package/create-route.mjs +62 -125
- package/create-router.mjs +50 -3
- package/index.mjs +2 -0
- package/package.json +6 -2
- package/rewrite.d.ts +61 -0
- package/rewrite.mjs +176 -0
- package/tag-request.mjs +100 -0
- package/tag-response.mjs +73 -0
- package/test/exports.mjs +28 -0
- package/test/index.mjs +2 -0
- package/test/rewrite.mjs +202 -0
- package/test/route.mjs +37 -0
- package/test/router.mjs +81 -0
- package/test/tag-req-res.mjs +253 -0
- package/test/types/fixture.ts +54 -0
- package/tsconfig.json +1 -0
- package/types.d.ts +44 -0
- package/utility/parse-http-text.mjs +164 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Shared positional parser for tag-request.mjs / tag-response.mjs, and
|
|
2
|
+
// (via create-route.mjs's function-substitution pre-pass) create-route.mjs.
|
|
3
|
+
//
|
|
4
|
+
// The bugs this exists to fix: create-request.mjs flattens a whole tagged
|
|
5
|
+
// template to one string before splitting on "\n" (and splits header
|
|
6
|
+
// lines on the literal substring ": ", truncating any value containing a
|
|
7
|
+
// second one); create-route.mjs re-feeds a stringified substitution back
|
|
8
|
+
// through its own line tokenizer, and uses headers.set() instead of
|
|
9
|
+
// .append(), so a second Set-Cookie line overwrites the first instead of
|
|
10
|
+
// accumulating. In all three cases, a substituted value containing "\n"
|
|
11
|
+
// or ":" can get reinterpreted as if the *caller* had typed new HTTP
|
|
12
|
+
// syntax.
|
|
13
|
+
//
|
|
14
|
+
// The fix: only ever scan the template's literal `strings` pieces for
|
|
15
|
+
// structure (the start line, header lines, the blank line that ends
|
|
16
|
+
// them). A substitution's value is appended verbatim to whatever's
|
|
17
|
+
// currently being built -- never re-split, never re-scanned -- so it
|
|
18
|
+
// can't introduce a header line, end the headers early, or do anything
|
|
19
|
+
// else the literal text didn't already set up.
|
|
20
|
+
const BINARY_BODY_TYPES = (value) =>
|
|
21
|
+
value instanceof Blob ||
|
|
22
|
+
value instanceof Uint8Array ||
|
|
23
|
+
value instanceof ArrayBuffer ||
|
|
24
|
+
value instanceof ReadableStream ||
|
|
25
|
+
value instanceof FormData ||
|
|
26
|
+
value instanceof URLSearchParams;
|
|
27
|
+
|
|
28
|
+
// options.isStartLine(line): called once, on the first complete line, to
|
|
29
|
+
// decide whether it's a real start line (request-line / status-line) or
|
|
30
|
+
// whether this template has none at all and that line is actually the
|
|
31
|
+
// first line of the body -- create-route.mjs's templates can be
|
|
32
|
+
// headers-and-body-only or body-only, falling back to its `init` object
|
|
33
|
+
// for status/headers in that case. Defaults to "yes, always" for
|
|
34
|
+
// tagRequest/tagResponse, which require a real start line.
|
|
35
|
+
// options.trimBodyLines: create-route.mjs's historical behavior trims
|
|
36
|
+
// *every* line, including body lines, before accumulating them (so an
|
|
37
|
+
// indented template literal's body doesn't carry that indentation into
|
|
38
|
+
// the real response body). tagRequest/tagResponse deliberately do NOT do
|
|
39
|
+
// this -- body content there is literal, preserved exactly as written,
|
|
40
|
+
// since trimming could corrupt meaningful whitespace in a text body.
|
|
41
|
+
// Defaults to false (the strict behavior); create-route.mjs passes true.
|
|
42
|
+
export const parseHttpText = (strings, substitutions, options = {}) => {
|
|
43
|
+
const { isStartLine = () => true, trimBodyLines = false } = options;
|
|
44
|
+
|
|
45
|
+
const stringsArr = [...strings];
|
|
46
|
+
stringsArr[0] = stringsArr[0].replace(/^[ \t\r\n]+/, "");
|
|
47
|
+
stringsArr[stringsArr.length - 1] = stringsArr[stringsArr.length - 1].replace(
|
|
48
|
+
/[ \t\r\n]+$/,
|
|
49
|
+
""
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
let zone = "start"; // "start" -> "headers" -> "body"
|
|
53
|
+
let lineBuffer = "";
|
|
54
|
+
// A binary substitution can't be classified as "body content" the
|
|
55
|
+
// moment it's seen while zone is "start" -- that line might turn out to
|
|
56
|
+
// be a real start line (invalid) or, once nothing else precedes it,
|
|
57
|
+
// the first line of a body-only template (valid, same as it landing in
|
|
58
|
+
// the body zone directly). Held here until the line is flushed and its
|
|
59
|
+
// true zone is known.
|
|
60
|
+
let pendingBinary = null;
|
|
61
|
+
let startLine = null;
|
|
62
|
+
const headerEntries = []; // ordered [{ name, value }], duplicates allowed
|
|
63
|
+
let bodyText = "";
|
|
64
|
+
let binaryBody = null;
|
|
65
|
+
let binaryBodyCount = 0;
|
|
66
|
+
let bodyHasOtherContent = false;
|
|
67
|
+
|
|
68
|
+
const commitBodyLine = (rawLine, isFinal) => {
|
|
69
|
+
const line = trimBodyLines ? rawLine.trim() : rawLine;
|
|
70
|
+
if (pendingBinary) {
|
|
71
|
+
if (line.trim() !== "" || bodyHasOtherContent) {
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
"parseHttpText: a binary body substitution must be the sole content of the body zone -- it can't be combined with literal text or another substitution"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
binaryBodyCount++;
|
|
77
|
+
binaryBody = pendingBinary;
|
|
78
|
+
pendingBinary = null;
|
|
79
|
+
} else {
|
|
80
|
+
bodyText += isFinal ? line : line + "\n";
|
|
81
|
+
if (line.trim() !== "") bodyHasOtherContent = true;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const flushLine = () => {
|
|
86
|
+
if (zone === "start") {
|
|
87
|
+
if (isStartLine(lineBuffer)) {
|
|
88
|
+
if (pendingBinary) {
|
|
89
|
+
throw new TypeError(
|
|
90
|
+
"parseHttpText: a binary substitution cannot appear within the start line"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
startLine = lineBuffer;
|
|
94
|
+
zone = "headers";
|
|
95
|
+
} else {
|
|
96
|
+
zone = "body";
|
|
97
|
+
commitBodyLine(lineBuffer, false);
|
|
98
|
+
}
|
|
99
|
+
} else if (zone === "headers") {
|
|
100
|
+
if (pendingBinary) {
|
|
101
|
+
throw new TypeError(
|
|
102
|
+
"parseHttpText: a binary substitution cannot appear within a header line"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (lineBuffer.trim() === "") {
|
|
106
|
+
zone = "body";
|
|
107
|
+
} else {
|
|
108
|
+
const colonIndex = lineBuffer.indexOf(":");
|
|
109
|
+
if (colonIndex > 0) {
|
|
110
|
+
headerEntries.push({
|
|
111
|
+
name: lineBuffer.slice(0, colonIndex).trim(),
|
|
112
|
+
value: lineBuffer.slice(colonIndex + 1).trim(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
lineBuffer = "";
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < stringsArr.length; i++) {
|
|
121
|
+
const parts = stringsArr[i].split("\n");
|
|
122
|
+
for (let p = 0; p < parts.length; p++) {
|
|
123
|
+
if (p > 0) {
|
|
124
|
+
if (zone === "body") {
|
|
125
|
+
commitBodyLine(lineBuffer, false);
|
|
126
|
+
} else {
|
|
127
|
+
flushLine();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
lineBuffer += parts[p];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (i < substitutions.length) {
|
|
134
|
+
const sub = substitutions[i];
|
|
135
|
+
if (sub instanceof Headers) {
|
|
136
|
+
for (const [name, value] of sub) headerEntries.push({ name, value });
|
|
137
|
+
} else if (zone !== "headers" && BINARY_BODY_TYPES(sub)) {
|
|
138
|
+
if (pendingBinary) {
|
|
139
|
+
throw new TypeError(
|
|
140
|
+
"parseHttpText: more than one binary body substitution found -- a binary value must be the sole content of the body"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
pendingBinary = sub;
|
|
144
|
+
} else if (sub !== undefined && sub !== null) {
|
|
145
|
+
if (zone === "body") bodyHasOtherContent = true;
|
|
146
|
+
lineBuffer += String(sub);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (zone === "body") {
|
|
152
|
+
commitBodyLine(lineBuffer, true);
|
|
153
|
+
} else {
|
|
154
|
+
flushLine();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
startLine,
|
|
159
|
+
headerEntries,
|
|
160
|
+
body: binaryBodyCount === 1 ? binaryBody : bodyText.trim(),
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default parseHttpText;
|