// Downlevel mode's JSX output, the classic runtime: `factory(tag, props | null, ...children)`, matching // ts.transpileModule with `jsx: "react"`. strip.zena parses the JSX and calls these through the jsx* hooks, // so only build/downlevel.wasm links this file. Every inserted text is free of line breaks; a string that spans // lines in the source closes before each line break and continues after it (`"a"+" b"`). import { Memory } from 'zena:memory'; import { delta } from './lex.zena'; import { outReserve, outCommit } from './strip.zena'; import { JSX_ENT } from './keywords.zena'; // The factory names: the options' (`pragma`), then the file's `@jsx`/`@jsxFrag` comment pragmas, as bytes. // A comment pragma's length is -1 before one is seen and 0 when the first one seen is not an entity name, which // TypeScript ignores in favor of the option (it never looks at a second one). var pr: FixedArray = new FixedArray(128, 0); var prLen: i32 = 0; var factoryAt: i32 = 0; var factoryLen: i32 = 0; var fragAt: i32 = 0; var fragLen: i32 = 0; var cFactoryAt: i32 = 0; var cFactoryLen: i32 = -1; var cFragAt: i32 = 0; var cFragLen: i32 = -1; // Kept text segments of the JSX text being written: [start, end) pairs. var seg: FixedArray = new FixedArray(64, 0); var segN: i32 = 0; function prPush(c: i32): void { if (prLen == pr.length) { let b = new FixedArray(pr.length * 2, 0); var i = 0; while (i < prLen) { b[i] = pr[i]; i = i + 1; } pr = b; } pr[prLen] = c; prLen = prLen + 1; } // The ABI's pragma(): `a` bytes of the factory, then `b` of the fragment factory, at addr. Starts a stream's // factories, so it also forgets the previous stream's comment pragmas. export function pragma(addr: i32, a: i32, b: i32): void { prLen = 0; factoryAt = 0; factoryLen = a; fragAt = a; fragLen = b; var i = 0; while (i < a + b) { prPush(Memory.default.getU8(addr + i)); i = i + 1; } cFactoryLen = -1; cFragLen = -1; } function inByte(p: i32): i32 { return Memory.default.getU8(p + delta); } function isLt(c: i32): boolean { return c == 10 || c == 13; } // ASCII whitespace, as the pragma regex's `\s` sees it. function isSpace(c: i32): boolean { return c == 32 || c == 9 || c == 10 || c == 13 || c == 11 || c == 12; } function isIdByte(c: i32, start: boolean): boolean { if ((c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c == 95 || c == 36 || c >= 0x80) return true; return !start && c >= 48 && c <= 57; } // Input [a, b) is an entity name, `a` or `a.b.c`: what TypeScript's parseIsolatedEntityName accepts. function isEntityName(a: i32, b: i32): boolean { var start = true; var q = a; while (q < b) { let c = inByte(q); if (c == 46) { if (start) return false; start = true; } else { if (!isIdByte(c, start)) return false; start = false; } q = q + 1; } return !start; } // Input [a, b) is `word`, ignoring ASCII case: 1 for `jsx`, 2 for `jsxfrag`, else 0. function pragmaKind(a: i32, b: i32): i32 { let n = b - a; if (n != 3 && n != 7) return 0; if ((inByte(a) | 32) != 106 || (inByte(a + 1) | 32) != 115 || (inByte(a + 2) | 32) != 120) return 0; if (n == 3) return 1; if ((inByte(a + 3) | 32) != 102 || (inByte(a + 4) | 32) != 114 || (inByte(a + 5) | 32) != 97 || (inByte(a + 6) | 32) != 103) return 0; return 2; } // TypeScript's processCommentPragmas over the leading comments [0, end) of the file: in each block comment, // `/@(\S+)(\s+(?:\S.*)?)?$/gm`, where the factory is the first word after the name. export function pragmas(end: i32): void { var p = 0; if (end >= 2 && inByte(0) == 35 && inByte(1) == 33) { while (p < end && !isLt(inByte(p))) p = p + 1; } while (p + 1 < end) { let c = inByte(p); if (c == 47 && inByte(p + 1) == 42) { var q = p + 2; while (q + 1 < end && !(inByte(q) == 42 && inByte(q + 1) == 47)) q = q + 1; q = q + 2; if (q > end) q = end; commentPragmas(p, q); p = q; } else if (c == 47 && inByte(p + 1) == 47) { while (p < end && !isLt(inByte(p))) p = p + 1; } else { p = p + 1; } } } function commentPragmas(a: i32, b: i32): void { var p = a; while (p < b) { if (inByte(p) != 64) { p = p + 1; continue; } var q = p + 1; while (q < b && !isSpace(inByte(q))) q = q + 1; if (q == p + 1) { p = p + 1; continue; } let kind = pragmaKind(p + 1, q); var r = q; while (r < b && isSpace(inByte(r))) r = r + 1; let arg = r; while (r < b && !isSpace(inByte(r))) r = r + 1; if (arg < r) { if (kind == 1 && cFactoryLen < 0) { cFactoryAt = prLen; cFactoryLen = savePragma(arg, r); } else if (kind == 2 && cFragLen < 0) { cFragAt = prLen; cFragLen = savePragma(arg, r); } } while (r < b && !isLt(inByte(r))) r = r + 1; p = r; } } // Copies input [a, b) after the stored names and returns its length, or 0 when it is not an entity name. function savePragma(a: i32, b: i32): i32 { if (!isEntityName(a, b)) return 0; var q = a; while (q < b) { prPush(inByte(q)); q = q + 1; } return b - a; } // --------------------------------------------------------------------------- // Output. function put(c: i32): void { Memory.default.setU8(outReserve(1), c); outCommit(1); } function putIn(a: i32, b: i32): void { var q = a; while (q < b) { put(inByte(q)); q = q + 1; } } function putPr(at: i32, n: i32): void { var i = 0; while (i < n) { put(pr[at + i]); i = i + 1; } } // The line terminators of input [a, b), and nothing else: what keeps the source's lines. function lines(a: i32, b: i32): void { var q = a; while (q < b) { let c = inByte(q); if (isLt(c)) { put(c); } else if (c == 0xe2 && q + 2 < b && inByte(q + 1) == 0x80 && (inByte(q + 2) | 1) == 0xa9) { putIn(q, q + 3); q = q + 2; } q = q + 1; } } function putNull(): void { put(110); put(117); put(108); put(108); } // `factory(`, then for a fragment `fragmentFactory,null`. export function open(frag: boolean): void { if (cFactoryLen > 0) putPr(cFactoryAt, cFactoryLen); else putPr(factoryAt, factoryLen); put(40); if (!frag) return; if (cFragLen > 0) putPr(cFragAt, cFragLen); else putPr(fragAt, fragLen); put(44); putNull(); } // Input [a, b), a tag or attribute name, as a string. export function quoted(a: i32, b: i32): void { put(34); putIn(a, b); put(34); } // `ns:name` as one string; any line terminators between the two parts follow it. export function ns(a1: i32, b1: i32, a2: i32, b2: i32): void { put(34); putIn(a1, b1); put(58); putIn(a2, b2); put(34); lines(b1, a2); } // strip.zena's JX_* pieces. export function piece(k: i32): void { if (k == 0) { put(44); } else if (k == 1) { put(44); put(123); } else if (k == 2) { put(125); } else if (k == 3) { put(44); putNull(); } else if (k == 4) { put(41); } else if (k == 5) { put(58); } else if (k == 6) { put(58); put(116); put(114); put(117); put(101); } else if (k == 7) { put(44); put(40); } else if (k == 8) { put(40); } else { put(59); } } // --------------------------------------------------------------------------- // Strings. // The code point of the UTF-8 sequence at input q, and its length (1 to 4) in seqN. var seqN: i32 = 1; function codePointAt(q: i32): i32 { let c = inByte(q); if (c < 0x80) { seqN = 1; return c; } if (c < 0xe0) { seqN = 2; return ((c & 0x1f) << 6) | (inByte(q + 1) & 0x3f); } if (c < 0xf0) { seqN = 3; return ((c & 0x0f) << 12) | ((inByte(q + 1) & 0x3f) << 6) | (inByte(q + 2) & 0x3f); } seqN = 4; return ((c & 0x07) << 18) | ((inByte(q + 1) & 0x3f) << 12) | ((inByte(q + 2) & 0x3f) << 6) | (inByte(q + 3) & 0x3f); } // TypeScript's isLineBreak and isWhiteSpaceSingleLine. function isLineBreak(cp: i32): boolean { return cp == 10 || cp == 13 || cp == 0x2028 || cp == 0x2029; } function isSingleLineSpace(cp: i32): boolean { if (cp == 32 || cp == 9 || cp == 11 || cp == 12) return true; if (cp < 0x85) return false; return cp == 0x85 || cp == 0xa0 || cp == 0x1680 || (cp >= 0x2000 && cp <= 0x200b) || cp == 0x202f || cp == 0x205f || cp == 0x3000 || cp == 0xfeff; } function hexDigit(d: i32): i32 { if (d < 10) return 48 + d; return 87 + d; } // `\u{X}`. function putCodePoint(cp: i32): void { put(92); put(117); put(123); var shift = 20; while (shift > 0 && ((cp >> shift) & 15) == 0) shift = shift - 4; while (shift >= 0) { put(hexDigit((cp >> shift) & 15)); shift = shift - 4; } put(125); } // TypeScript's entities table (engine/jsx-entities.wat): the code point of input [a, b), or -1. function entityCodePoint(a: i32, b: i32): i32 { var r = JSX_ENT; while (true) { let n = Memory.default.getU8(r); if (n == 0) return -1; if (n == b - a) { var i = 0; while (i < n && Memory.default.getU8(r + 1 + i) == inByte(a + i)) i = i + 1; if (i == n) { let v = r + 1 + n; return Memory.default.getU8(v) | (Memory.default.getU8(v + 1) << 8) | (Memory.default.getU8(v + 2) << 16); } } r = r + n + 4; } } function isWordByte(c: i32): boolean { return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c == 95; } // TypeScript's decodeEntities at the `&` at input q, before e: `{`, `{` or `&name;`. Writes the code // point and returns the end, or returns q when there is none. function entity(q: i32, e: i32): i32 { var r = q + 1; if (r < e && inByte(r) == 35) { r = r + 1; var hex = false; if (r < e && inByte(r) == 120) { hex = true; r = r + 1; } let d0 = r; var v = 0; while (r < e) { let c = inByte(r); var d = -1; if (c >= 48 && c <= 57) d = c - 48; else if (hex && (c | 32) >= 97 && (c | 32) <= 102) d = (c | 32) - 87; if (d < 0) break; if (v < 0x1000000) { if (hex) v = v * 16 + d; else v = v * 10 + d; } r = r + 1; } if (r == d0 || r >= e || inByte(r) != 59) return q; putCodePoint(v); return r + 1; } let w = r; while (r < e && isWordByte(inByte(r))) r = r + 1; if (r == w || r >= e || inByte(r) != 59) return q; let cp = entityCodePoint(w, r); if (cp < 0) return q; putCodePoint(cp); return r + 1; } // The body of a string with value input [a, b) (entities decoded), escaped for double quotes. With `split`, a line // terminator (CRLF as one) becomes its escape, then `"+`, the terminator itself, and `"`. function strBody(a: i32, b: i32, split: boolean): void { var q = a; while (q < b) { let c = inByte(q); if (c == 38) { let r = entity(q, b); if (r > q) { q = r; continue; } } if (c == 34 || c == 92) { put(92); put(c); q = q + 1; continue; } var n = 0; if (split && isLt(c)) n = 1; if (n == 1 && c == 13 && q + 1 < b && inByte(q + 1) == 10) n = 2; if (split && c == 0xe2 && q + 2 < b && inByte(q + 1) == 0x80 && (inByte(q + 2) | 1) == 0xa9) n = 3; if (n == 0) { put(c); q = q + 1; continue; } if (c == 10) { put(92); put(110); } else if (c == 13) { put(92); put(114); if (n == 2) { put(92); put(110); } } else { putCodePoint(codePointAt(q)); } put(34); put(43); putIn(q, q + n); put(34); q = q + n; } } // An attribute string, input [a, b) including its quotes: no escapes in the source, entities decoded. export function str(a: i32, b: i32): void { put(34); strBody(a + 1, b - 1, true); put(34); } function addSeg(a: i32, b: i32): void { if (segN * 2 + 2 > seg.length) { let next = new FixedArray(seg.length * 2, 0); var i = 0; while (i < segN * 2) { next[i] = seg[i]; i = i + 1; } seg = next; } seg[segN * 2] = a; seg[segN * 2 + 1] = b; segN = segN + 1; } // A JSX text child, input [a, b), as `,"..."`, or nothing when it is only whitespace across lines. TypeScript's // fixupWhitespaceAndDecodeEntities: each line is trimmed (except the start of the first and the end of the // last), whitespace-only lines are dropped, and the rest are joined by one space. export function text(a: i32, b: i32): void { segN = 0; var first = a; var lastEnd = -1; var q = a; while (q < b) { let cp = codePointAt(q); let n = seqN; if (isLineBreak(cp)) { if (first >= 0 && lastEnd >= 0) addSeg(first, lastEnd); first = -1; } else if (!isSingleLineSpace(cp)) { lastEnd = q + n; if (first < 0) first = q; } q = q + n; } if (first >= 0) addSeg(first, b); if (segN == 0) { lines(a, b); return; } var p = a; var i = 0; while (i < segN) { let s = seg[i * 2]; if (i == 0) { lines(p, s); put(44); put(34); } else { put(34); put(43); lines(p, s); put(34); put(32); } p = seg[i * 2 + 1]; strBody(s, p, false); i = i + 1; } put(34); lines(p, b); }