mirror of
https://github.com/oven-sh/bun
synced 2026-03-02 13:31:01 +01:00
Compare commits
3 Commits
main
...
jarred/mar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65bb2c68ed | ||
|
|
cad86ad550 | ||
|
|
a87b8e10ad |
@@ -124,22 +124,32 @@ Return a string to replace the element's rendering. Return `null` or `undefined`
|
||||
|
||||
### Block callbacks
|
||||
|
||||
| Callback | Meta | Description |
|
||||
| ------------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `heading` | `{ level: number, id?: string }` | Heading level 1–6. `id` is set when `headings: { ids: true }` is enabled |
|
||||
| `paragraph` | — | Paragraph block |
|
||||
| `blockquote` | — | Blockquote block |
|
||||
| `code` | `{ language?: string }` | Fenced or indented code block. `language` is the info-string when specified on the fence |
|
||||
| `list` | `{ ordered: boolean, start?: number }` | Ordered or unordered list. `start` is the start number for ordered lists |
|
||||
| `listItem` | `{ checked?: boolean }` | List item. `checked` is set for task list items (`- [x]` / `- [ ]`) |
|
||||
| `hr` | — | Horizontal rule |
|
||||
| `table` | — | Table block |
|
||||
| `thead` | — | Table head |
|
||||
| `tbody` | — | Table body |
|
||||
| `tr` | — | Table row |
|
||||
| `th` | `{ align?: "left" \| "center" \| "right" }` | Table header cell. `align` is set when alignment is specified |
|
||||
| `td` | `{ align?: "left" \| "center" \| "right" }` | Table data cell. `align` is set when alignment is specified |
|
||||
| `html` | — | Raw HTML content |
|
||||
| Callback | Meta | Description |
|
||||
| ------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `heading` | `{ level, id? }` | Heading level 1–6. `id` is set when `headings: { ids: true }` is enabled |
|
||||
| `paragraph` | — | Paragraph block |
|
||||
| `blockquote` | — | Blockquote block |
|
||||
| `code` | `{ language? }` | Fenced or indented code block. `language` is the info-string when specified on the fence |
|
||||
| `list` | `{ ordered, start?, depth }` | `depth` is nesting level (0 = top-level). `start` is set for ordered lists |
|
||||
| `listItem` | `{ index, depth, ordered, start?, checked? }` | See [List item meta](#list-item-meta) below |
|
||||
| `hr` | — | Horizontal rule |
|
||||
| `table` | — | Table block |
|
||||
| `thead` | — | Table head |
|
||||
| `tbody` | — | Table body |
|
||||
| `tr` | — | Table row |
|
||||
| `th` | `{ align? }` | Table header cell. `align` is `"left"`, `"center"`, `"right"`, or absent |
|
||||
| `td` | `{ align? }` | Table data cell |
|
||||
| `html` | — | Raw HTML content |
|
||||
|
||||
#### List item meta
|
||||
|
||||
The `listItem` callback receives everything needed to render markers directly:
|
||||
|
||||
- `index` — 0-based position within the parent list
|
||||
- `depth` — the parent list's nesting level (0 = top-level)
|
||||
- `ordered` — whether the parent list is ordered
|
||||
- `start` — the parent list's start number (only when `ordered` is true)
|
||||
- `checked` — task list state (only for `- [x]` / `- [ ]` items)
|
||||
|
||||
### Inline callbacks
|
||||
|
||||
@@ -205,6 +215,33 @@ const ansi = Bun.markdown.render("# Hello\n\nThis is **bold** and *italic*", {
|
||||
});
|
||||
```
|
||||
|
||||
#### Nested list numbering
|
||||
|
||||
The `listItem` callback receives everything needed to render markers directly — no post-processing:
|
||||
|
||||
```ts
|
||||
const result = Bun.markdown.render("1. first\n 1. sub-a\n 2. sub-b\n2. second", {
|
||||
listItem: (children, { index, depth, ordered, start }) => {
|
||||
const n = (start ?? 1) + index;
|
||||
// 1. 2. 3. at depth 0, a. b. c. at depth 1, i. ii. iii. at depth 2
|
||||
const marker = !ordered
|
||||
? "-"
|
||||
: depth === 0
|
||||
? `${n}.`
|
||||
: depth === 1
|
||||
? `${String.fromCharCode(96 + n)}.`
|
||||
: `${toRoman(n)}.`;
|
||||
return " ".repeat(depth) + marker + " " + children.trimEnd() + "\n";
|
||||
},
|
||||
// Prepend a newline so nested lists are separated from their parent item's text
|
||||
list: children => "\n" + children,
|
||||
});
|
||||
// 1. first
|
||||
// a. sub-a
|
||||
// b. sub-b
|
||||
// 2. second
|
||||
```
|
||||
|
||||
#### Code block syntax highlighting
|
||||
|
||||
````ts
|
||||
|
||||
14
packages/bun-types/bun.d.ts
vendored
14
packages/bun-types/bun.d.ts
vendored
@@ -1193,10 +1193,20 @@ declare module "bun" {
|
||||
ordered: boolean;
|
||||
/** The start number for ordered lists. */
|
||||
start?: number;
|
||||
/** Nesting depth. `0` for a top-level list, `1` for a list inside a list item, etc. */
|
||||
depth: number;
|
||||
}
|
||||
|
||||
/** Meta passed to the `listItem` callback. */
|
||||
interface ListItemMeta {
|
||||
/** 0-based index of this item within its parent list. */
|
||||
index: number;
|
||||
/** Nesting depth of the parent list. `0` for items in a top-level list. */
|
||||
depth: number;
|
||||
/** Whether the parent list is ordered. */
|
||||
ordered: boolean;
|
||||
/** The start number of the parent list (only set when `ordered` is true). */
|
||||
start?: number;
|
||||
/** Task list checked state. Set for `- [x]` / `- [ ]` items. */
|
||||
checked?: boolean;
|
||||
}
|
||||
@@ -1234,8 +1244,8 @@ declare module "bun" {
|
||||
code?: (children: string, meta?: CodeBlockMeta) => string | null | undefined;
|
||||
/** Ordered or unordered list. `start` is the first item number for ordered lists. */
|
||||
list?: (children: string, meta: ListMeta) => string | null | undefined;
|
||||
/** List item. `meta.checked` is set for task list items (`- [x]` / `- [ ]`). Only passed for task list items. */
|
||||
listItem?: (children: string, meta?: ListItemMeta) => string | null | undefined;
|
||||
/** List item. `meta` always includes `{index, depth, ordered}`. `meta.start` is set for ordered lists; `meta.checked` is set for task list items. */
|
||||
listItem?: (children: string, meta: ListItemMeta) => string | null | undefined;
|
||||
/** Horizontal rule. */
|
||||
hr?: (children: string) => string | null | undefined;
|
||||
/** Table. */
|
||||
|
||||
@@ -32,102 +32,120 @@ namespace uWS {
|
||||
constexpr uint64_t STATE_HAS_SIZE = 1ull << (sizeof(uint64_t) * 8 - 1);//0x8000000000000000;
|
||||
constexpr uint64_t STATE_IS_CHUNKED = 1ull << (sizeof(uint64_t) * 8 - 2);//0x4000000000000000;
|
||||
constexpr uint64_t STATE_IS_CHUNKED_EXTENSION = 1ull << (sizeof(uint64_t) * 8 - 3);//0x2000000000000000;
|
||||
constexpr uint64_t STATE_WAITING_FOR_LF = 1ull << (sizeof(uint64_t) * 8 - 4);//0x1000000000000000;
|
||||
constexpr uint64_t STATE_SIZE_MASK = ~(STATE_HAS_SIZE | STATE_IS_CHUNKED | STATE_IS_CHUNKED_EXTENSION | STATE_WAITING_FOR_LF);//0x0FFFFFFFFFFFFFFF;
|
||||
constexpr uint64_t STATE_SIZE_MASK = ~(STATE_HAS_SIZE | STATE_IS_CHUNKED | STATE_IS_CHUNKED_EXTENSION);//0x1FFFFFFFFFFFFFFF;
|
||||
constexpr uint64_t STATE_IS_ERROR = ~0ull;//0xFFFFFFFFFFFFFFFF;
|
||||
/* Overflow guard: if any of bits 55-59 are set before the next *16, one more
|
||||
* hex digit (plus the +2 for the trailing CRLF of chunk-data) would carry into
|
||||
* STATE_WAITING_FOR_LF at bit 60. Limits chunk size to 14 hex digits (~72 PB). */
|
||||
constexpr uint64_t STATE_SIZE_OVERFLOW = 0x1Full << (sizeof(uint64_t) * 8 - 9);//0x0F80000000000000;
|
||||
constexpr uint64_t STATE_SIZE_OVERFLOW = 0x0Full << (sizeof(uint64_t) * 8 - 8);//0x0F00000000000000;
|
||||
|
||||
inline uint64_t chunkSize(uint64_t state) {
|
||||
return state & STATE_SIZE_MASK;
|
||||
}
|
||||
|
||||
/* Parses the chunk-size line: HEXDIG+ [;ext...] CRLF
|
||||
*
|
||||
* Returns the new state. On return, exactly one of:
|
||||
* - state has STATE_HAS_SIZE set (success, data advanced past LF)
|
||||
* - state == STATE_IS_ERROR (malformed input)
|
||||
* - data is empty (short read; flags persist for resume)
|
||||
*
|
||||
* Resume flags:
|
||||
* STATE_WAITING_FOR_LF -> saw '\r' on previous call, need '\n'
|
||||
* STATE_IS_CHUNKED_EXTENSION -> mid-extension, skip hex parsing on resume
|
||||
*
|
||||
* Structure follows upstream uWS (scan-for-LF) with strict CRLF validation
|
||||
* added. Every byte is consumed in a forward scan so TCP segment boundaries
|
||||
* splitting the line at any point are handled by construction.
|
||||
*
|
||||
* RFC 7230 4.1.1:
|
||||
* chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF
|
||||
* chunk-size = 1*HEXDIG
|
||||
* chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
|
||||
* chunk-ext-name = token
|
||||
* chunk-ext-val = token / quoted-string (TODO: quoted-string unsupported)
|
||||
*/
|
||||
inline uint64_t consumeHexNumber(std::string_view &data, uint64_t state) {
|
||||
/* Resume: '\r' was the last byte of the previous segment. Rare path,
|
||||
* use data directly to avoid the p/len load on the hot path. */
|
||||
if (state & STATE_WAITING_FOR_LF) [[unlikely]] {
|
||||
if (!data.length()) return state;
|
||||
if (data[0] != '\n') return STATE_IS_ERROR;
|
||||
data.remove_prefix(1);
|
||||
return ((state & ~(STATE_WAITING_FOR_LF | STATE_IS_CHUNKED_EXTENSION)) + 2)
|
||||
| STATE_HAS_SIZE | STATE_IS_CHUNKED;
|
||||
}
|
||||
inline bool isParsingChunkedExtension(uint64_t state) {
|
||||
return (state & STATE_IS_CHUNKED_EXTENSION) != 0;
|
||||
}
|
||||
|
||||
/* Load pointer+length into locals so the loops operate in registers.
|
||||
* Without this, Clang writes back to the string_view on every iteration.
|
||||
* Error paths skip the writeback: HttpParser returns immediately on
|
||||
* STATE_IS_ERROR and never reads data. */
|
||||
const char *p = data.data();
|
||||
size_t len = data.length();
|
||||
/* Reads hex number until CR or out of data to consume. Updates state. Returns bytes consumed. */
|
||||
inline void consumeHexNumber(std::string_view &data, uint64_t &state) {
|
||||
|
||||
/* Hex digits. Skipped when resuming mid-extension so that extension bytes
|
||||
* like 'a' aren't misparsed as hex. */
|
||||
if (!(state & STATE_IS_CHUNKED_EXTENSION)) {
|
||||
while (len) {
|
||||
unsigned char c = (unsigned char) *p;
|
||||
if (c <= 32 || c == ';') break; /* fall through to drain loop */
|
||||
unsigned int d = c | 0x20; /* fold A-F -> a-f; '0'..'9' unchanged */
|
||||
unsigned int n;
|
||||
if ((unsigned)(d - '0') < 10) [[likely]] n = d - '0';
|
||||
else if ((unsigned)(d - 'a') < 6) n = d - 'a' + 10;
|
||||
else return STATE_IS_ERROR;
|
||||
if (chunkSize(state) & STATE_SIZE_OVERFLOW) [[unlikely]] return STATE_IS_ERROR;
|
||||
state = ((state & STATE_SIZE_MASK) * 16ull + n) | STATE_IS_CHUNKED;
|
||||
++p; --len;
|
||||
}
|
||||
}
|
||||
/* RFC 9110: 5.5 Field Values (TLDR; anything above 31 is allowed \r, \n ; depending on context)*/
|
||||
|
||||
/* Drain [;ext...] \r \n. Upstream-style forward scan for LF, with strict
|
||||
* validation: only >32 bytes (extension) and exactly one '\r' immediately
|
||||
* before '\n' are allowed. */
|
||||
while (len) {
|
||||
unsigned char c = (unsigned char) *p;
|
||||
if (c == '\n') return STATE_IS_ERROR; /* bare LF */
|
||||
++p; --len;
|
||||
if (c == '\r') {
|
||||
if (!len) {
|
||||
data = std::string_view(p, len);
|
||||
return state | STATE_WAITING_FOR_LF;
|
||||
if(!isParsingChunkedExtension(state)){
|
||||
/* Consume everything higher than 32 and not ; (extension)*/
|
||||
while (data.length() && data[0] > 32 && data[0] != ';') {
|
||||
|
||||
unsigned char digit = (unsigned char)data[0];
|
||||
unsigned int number;
|
||||
if (digit >= '0' && digit <= '9') {
|
||||
number = digit - '0';
|
||||
} else if (digit >= 'a' && digit <= 'f') {
|
||||
number = digit - 'a' + 10;
|
||||
} else if (digit >= 'A' && digit <= 'F') {
|
||||
number = digit - 'A' + 10;
|
||||
} else {
|
||||
state = STATE_IS_ERROR;
|
||||
return;
|
||||
}
|
||||
if (*p != '\n') return STATE_IS_ERROR;
|
||||
++p; --len;
|
||||
data = std::string_view(p, len);
|
||||
return ((state & ~STATE_IS_CHUNKED_EXTENSION) + 2)
|
||||
| STATE_HAS_SIZE | STATE_IS_CHUNKED;
|
||||
|
||||
if ((chunkSize(state) & STATE_SIZE_OVERFLOW)) {
|
||||
state = STATE_IS_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
// extract state bits
|
||||
uint64_t bits = /*state &*/ STATE_IS_CHUNKED;
|
||||
|
||||
state = (state & STATE_SIZE_MASK) * 16ull + number;
|
||||
|
||||
state |= bits;
|
||||
data.remove_prefix(1);
|
||||
}
|
||||
if (c <= 32) return STATE_IS_ERROR;
|
||||
state |= STATE_IS_CHUNKED_EXTENSION;
|
||||
}
|
||||
data = std::string_view(p, len);
|
||||
return state; /* short read */
|
||||
|
||||
auto len = data.length();
|
||||
if(len) {
|
||||
// consume extension
|
||||
if(data[0] == ';' || isParsingChunkedExtension(state)) {
|
||||
// mark that we are parsing chunked extension
|
||||
state |= STATE_IS_CHUNKED_EXTENSION;
|
||||
/* we got chunk extension lets remove it*/
|
||||
while(data.length()) {
|
||||
if(data[0] == '\r') {
|
||||
// we are done parsing extension
|
||||
state &= ~STATE_IS_CHUNKED_EXTENSION;
|
||||
break;
|
||||
}
|
||||
/* RFC 9110: Token format (TLDR; anything bellow 32 is not allowed)
|
||||
* TODO: add support for quoted-strings values (RFC 9110: 3.2.6. Quoted-String)
|
||||
* Example of chunked encoding with extensions:
|
||||
*
|
||||
* 4;key=value\r\n
|
||||
* Wiki\r\n
|
||||
* 5;foo=bar;baz=quux\r\n
|
||||
* pedia\r\n
|
||||
* 0\r\n
|
||||
* \r\n
|
||||
*
|
||||
* The chunk size is in hex (4, 5, 0), followed by optional
|
||||
* semicolon-separated extensions. Extensions consist of a key
|
||||
* (token) and optional value. The value may be a token or a
|
||||
* quoted string. The chunk data follows the CRLF after the
|
||||
* extensions and must be exactly the size specified.
|
||||
*
|
||||
* RFC 7230 Section 4.1.1 defines chunk extensions as:
|
||||
* chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
|
||||
* chunk-ext-name = token
|
||||
* chunk-ext-val = token / quoted-string
|
||||
*/
|
||||
if(data[0] <= 32) {
|
||||
state = STATE_IS_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
data.remove_prefix(1);
|
||||
}
|
||||
}
|
||||
if(data.length() >= 2) {
|
||||
/* Consume \r\n */
|
||||
if((data[0] != '\r' || data[1] != '\n')) {
|
||||
state = STATE_IS_ERROR;
|
||||
return;
|
||||
}
|
||||
state += 2; // include the two last /r/n
|
||||
state |= STATE_HAS_SIZE | STATE_IS_CHUNKED;
|
||||
|
||||
data.remove_prefix(2);
|
||||
}
|
||||
}
|
||||
// short read
|
||||
}
|
||||
|
||||
inline void decChunkSize(uint64_t &state, uint64_t by) {
|
||||
|
||||
//unsigned int bits = state & STATE_IS_CHUNKED;
|
||||
|
||||
state = (state & ~STATE_SIZE_MASK) | (chunkSize(state) - by);
|
||||
|
||||
//state |= bits;
|
||||
}
|
||||
|
||||
inline bool hasChunkSize(uint64_t state) {
|
||||
@@ -169,8 +187,8 @@ namespace uWS {
|
||||
}
|
||||
|
||||
if (!hasChunkSize(state)) {
|
||||
state = consumeHexNumber(data, state);
|
||||
if (isParsingInvalidChunkedEncoding(state)) [[unlikely]] {
|
||||
consumeHexNumber(data, state);
|
||||
if (isParsingInvalidChunkedEncoding(state)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (hasChunkSize(state) && chunkSize(state) == 2) {
|
||||
@@ -186,10 +204,6 @@ namespace uWS {
|
||||
|
||||
return std::string_view(nullptr, 0);
|
||||
}
|
||||
if (!hasChunkSize(state)) [[unlikely]] {
|
||||
/* Incomplete chunk-size line — need more data from the network. */
|
||||
return std::nullopt;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -759,8 +759,12 @@ const JsCallbackRenderer = struct {
|
||||
|
||||
const StackEntry = struct {
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
block_type: md.BlockType = .doc,
|
||||
data: u32 = 0,
|
||||
flags: u32 = 0,
|
||||
/// For ul/ol: number of li children seen so far (next li's index).
|
||||
/// For li: this item's 0-based index within its parent list.
|
||||
child_index: u32 = 0,
|
||||
detail: md.SpanDetail = .{},
|
||||
};
|
||||
|
||||
@@ -853,7 +857,22 @@ const JsCallbackRenderer = struct {
|
||||
if (block_type == .h) {
|
||||
self.#heading_tracker.enterHeading();
|
||||
}
|
||||
try self.#stack.append(self.#allocator, .{ .data = data, .flags = flags });
|
||||
|
||||
// For li: record its 0-based index within the parent list, then
|
||||
// increment the parent's counter so the next sibling gets index+1.
|
||||
var child_index: u32 = 0;
|
||||
if (block_type == .li and self.#stack.items.len > 0) {
|
||||
const parent = &self.#stack.items[self.#stack.items.len - 1];
|
||||
child_index = parent.child_index;
|
||||
parent.child_index += 1;
|
||||
}
|
||||
|
||||
try self.#stack.append(self.#allocator, .{
|
||||
.block_type = block_type,
|
||||
.data = data,
|
||||
.flags = flags,
|
||||
.child_index = child_index,
|
||||
});
|
||||
}
|
||||
|
||||
fn leaveBlockImpl(ptr: *anyopaque, block_type: md.BlockType, _: u32) bun.JSError!void {
|
||||
@@ -986,6 +1005,30 @@ const JsCallbackRenderer = struct {
|
||||
// Metadata object creation
|
||||
// ========================================
|
||||
|
||||
/// Walks the stack to count enclosing ul/ol blocks. Called during leave,
|
||||
/// so the top entry is the block itself (skip it for li, count it for ul/ol's
|
||||
/// own depth which excludes self).
|
||||
fn countListDepth(self: *JsCallbackRenderer) u32 {
|
||||
var depth: u32 = 0;
|
||||
// Skip the top entry (self) — we want enclosing lists only.
|
||||
const len = self.#stack.items.len;
|
||||
if (len < 2) return 0;
|
||||
for (self.#stack.items[0 .. len - 1]) |entry| {
|
||||
if (entry.block_type == .ul or entry.block_type == .ol) depth += 1;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
/// Returns the parent ul/ol entry for the current li (top of stack).
|
||||
/// Returns null if the stack shape is unexpected.
|
||||
fn parentList(self: *JsCallbackRenderer) ?*const StackEntry {
|
||||
const len = self.#stack.items.len;
|
||||
if (len < 2) return null;
|
||||
const parent = &self.#stack.items[len - 2];
|
||||
if (parent.block_type == .ul or parent.block_type == .ol) return parent;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn createBlockMeta(self: *JsCallbackRenderer, block_type: md.BlockType, data: u32, flags: u32) bun.JSError!?JSValue {
|
||||
const g = self.#globalObject;
|
||||
switch (block_type) {
|
||||
@@ -1000,15 +1043,10 @@ const JsCallbackRenderer = struct {
|
||||
return obj;
|
||||
},
|
||||
.ol => {
|
||||
const obj = JSValue.createEmptyObject(g, 2);
|
||||
obj.put(g, ZigString.static("ordered"), .true);
|
||||
obj.put(g, ZigString.static("start"), JSValue.jsNumber(data));
|
||||
return obj;
|
||||
return BunMarkdownMeta__createList(g, true, JSValue.jsNumber(data), self.countListDepth());
|
||||
},
|
||||
.ul => {
|
||||
const obj = JSValue.createEmptyObject(g, 1);
|
||||
obj.put(g, ZigString.static("ordered"), .false);
|
||||
return obj;
|
||||
return BunMarkdownMeta__createList(g, false, .js_undefined, self.countListDepth());
|
||||
},
|
||||
.code => {
|
||||
if (flags & md.BLOCK_FENCED_CODE != 0) {
|
||||
@@ -1023,21 +1061,31 @@ const JsCallbackRenderer = struct {
|
||||
},
|
||||
.th, .td => {
|
||||
const alignment = md.types.alignmentFromData(data);
|
||||
if (md.types.alignmentName(alignment)) |align_str| {
|
||||
const obj = JSValue.createEmptyObject(g, 1);
|
||||
obj.put(g, ZigString.static("align"), try bun.String.createUTF8ForJS(g, align_str));
|
||||
return obj;
|
||||
}
|
||||
return null;
|
||||
const align_js = if (md.types.alignmentName(alignment)) |align_str|
|
||||
try bun.String.createUTF8ForJS(g, align_str)
|
||||
else
|
||||
JSValue.js_undefined;
|
||||
return BunMarkdownMeta__createCell(g, align_js);
|
||||
},
|
||||
.li => {
|
||||
// The li entry is still on top of the stack; parent ul/ol is at len-2.
|
||||
const len = self.#stack.items.len;
|
||||
const item_index = if (len > 1) self.#stack.items[len - 1].child_index else 0;
|
||||
const parent = self.parentList();
|
||||
const is_ordered = parent != null and parent.?.block_type == .ol;
|
||||
// countListDepth() includes the immediate parent list; subtract it
|
||||
// so that items in a top-level list report depth 0.
|
||||
const enclosing = self.countListDepth();
|
||||
const depth: u32 = if (enclosing > 0) enclosing - 1 else 0;
|
||||
const task_mark = md.types.taskMarkFromData(data);
|
||||
if (task_mark != 0) {
|
||||
const obj = JSValue.createEmptyObject(g, 1);
|
||||
obj.put(g, ZigString.static("checked"), JSValue.jsBoolean(md.types.isTaskChecked(task_mark)));
|
||||
return obj;
|
||||
}
|
||||
return null;
|
||||
|
||||
const start_js = if (is_ordered) JSValue.jsNumber(parent.?.data) else JSValue.js_undefined;
|
||||
const checked_js = if (task_mark != 0)
|
||||
JSValue.jsBoolean(md.types.isTaskChecked(task_mark))
|
||||
else
|
||||
JSValue.js_undefined;
|
||||
|
||||
return BunMarkdownMeta__createListItem(g, item_index, depth, is_ordered, start_js, checked_js);
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
@@ -1047,14 +1095,18 @@ const JsCallbackRenderer = struct {
|
||||
const g = self.#globalObject;
|
||||
switch (span_type) {
|
||||
.a => {
|
||||
const obj = JSValue.createEmptyObject(g, 2);
|
||||
obj.put(g, ZigString.static("href"), try bun.String.createUTF8ForJS(g, detail.href));
|
||||
if (detail.title.len > 0) {
|
||||
obj.put(g, ZigString.static("title"), try bun.String.createUTF8ForJS(g, detail.title));
|
||||
}
|
||||
return obj;
|
||||
const href = try bun.String.createUTF8ForJS(g, detail.href);
|
||||
const title = if (detail.title.len > 0)
|
||||
try bun.String.createUTF8ForJS(g, detail.title)
|
||||
else
|
||||
JSValue.js_undefined;
|
||||
return BunMarkdownMeta__createLink(g, href, title);
|
||||
},
|
||||
.img => {
|
||||
// Image meta shares shape with link (src/href are both the first
|
||||
// field). We use a separate cached structure would require a
|
||||
// second slot, so just fall back to the generic path here —
|
||||
// images are rare enough that it doesn't matter.
|
||||
const obj = JSValue.createEmptyObject(g, 2);
|
||||
obj.put(g, ZigString.static("src"), try bun.String.createUTF8ForJS(g, detail.href));
|
||||
if (detail.title.len > 0) {
|
||||
@@ -1114,6 +1166,14 @@ const TagIndex = enum(u8) {
|
||||
|
||||
extern fn BunMarkdownTagStrings__getTagString(*jsc.JSGlobalObject, u8) JSValue;
|
||||
|
||||
// Fast-path meta-object constructors using cached Structures (see
|
||||
// BunMarkdownMeta.cpp). Each constructs via putDirectOffset so the
|
||||
// resulting objects share a single Structure and stay monomorphic.
|
||||
extern fn BunMarkdownMeta__createListItem(*jsc.JSGlobalObject, u32, u32, bool, JSValue, JSValue) JSValue;
|
||||
extern fn BunMarkdownMeta__createList(*jsc.JSGlobalObject, bool, JSValue, u32) JSValue;
|
||||
extern fn BunMarkdownMeta__createCell(*jsc.JSGlobalObject, JSValue) JSValue;
|
||||
extern fn BunMarkdownMeta__createLink(*jsc.JSGlobalObject, JSValue, JSValue) JSValue;
|
||||
|
||||
fn getCachedTagString(globalObject: *jsc.JSGlobalObject, tag: TagIndex) JSValue {
|
||||
return BunMarkdownTagStrings__getTagString(globalObject, @intFromEnum(tag));
|
||||
}
|
||||
|
||||
123
src/bun.js/bindings/BunMarkdownMeta.cpp
Normal file
123
src/bun.js/bindings/BunMarkdownMeta.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#include "BunMarkdownMeta.h"
|
||||
|
||||
#include "JavaScriptCore/JSObjectInlines.h"
|
||||
#include "JavaScriptCore/ObjectConstructor.h"
|
||||
#include "JavaScriptCore/JSCast.h"
|
||||
|
||||
using namespace JSC;
|
||||
|
||||
namespace Bun {
|
||||
namespace MarkdownMeta {
|
||||
|
||||
// Builds a cached Structure with N fixed property offsets. Properties are
|
||||
// laid out in declaration order so the extern "C" create functions can use
|
||||
// putDirectOffset without name lookups.
|
||||
static Structure* buildStructure(VM& vm, JSGlobalObject* globalObject, std::initializer_list<ASCIILiteral> names)
|
||||
{
|
||||
Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(
|
||||
globalObject,
|
||||
globalObject->objectPrototype(),
|
||||
names.size());
|
||||
|
||||
PropertyOffset offset;
|
||||
PropertyOffset expected = 0;
|
||||
for (auto name : names) {
|
||||
structure = structure->addPropertyTransition(vm, structure, Identifier::fromString(vm, name), 0, offset);
|
||||
ASSERT_UNUSED(expected, offset == expected);
|
||||
expected++;
|
||||
}
|
||||
return structure;
|
||||
}
|
||||
|
||||
Structure* createListItemMetaStructure(VM& vm, JSGlobalObject* globalObject)
|
||||
{
|
||||
return buildStructure(vm, globalObject, { "index"_s, "depth"_s, "ordered"_s, "start"_s, "checked"_s });
|
||||
}
|
||||
|
||||
Structure* createListMetaStructure(VM& vm, JSGlobalObject* globalObject)
|
||||
{
|
||||
return buildStructure(vm, globalObject, { "ordered"_s, "start"_s, "depth"_s });
|
||||
}
|
||||
|
||||
Structure* createCellMetaStructure(VM& vm, JSGlobalObject* globalObject)
|
||||
{
|
||||
return buildStructure(vm, globalObject, { "align"_s });
|
||||
}
|
||||
|
||||
Structure* createLinkMetaStructure(VM& vm, JSGlobalObject* globalObject)
|
||||
{
|
||||
return buildStructure(vm, globalObject, { "href"_s, "title"_s });
|
||||
}
|
||||
|
||||
} // namespace MarkdownMeta
|
||||
} // namespace Bun
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// extern "C" constructors — callable from MarkdownObject.zig
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createListItem(
|
||||
JSGlobalObject* globalObject,
|
||||
uint32_t index,
|
||||
uint32_t depth,
|
||||
bool ordered,
|
||||
EncodedJSValue start,
|
||||
EncodedJSValue checked)
|
||||
{
|
||||
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
|
||||
VM& vm = global->vm();
|
||||
|
||||
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownListItemMetaStructure());
|
||||
obj->putDirectOffset(vm, 0, jsNumber(index));
|
||||
obj->putDirectOffset(vm, 1, jsNumber(depth));
|
||||
obj->putDirectOffset(vm, 2, jsBoolean(ordered));
|
||||
obj->putDirectOffset(vm, 3, JSValue::decode(start));
|
||||
obj->putDirectOffset(vm, 4, JSValue::decode(checked));
|
||||
|
||||
return JSValue::encode(obj);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createList(
|
||||
JSGlobalObject* globalObject,
|
||||
bool ordered,
|
||||
EncodedJSValue start,
|
||||
uint32_t depth)
|
||||
{
|
||||
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
|
||||
VM& vm = global->vm();
|
||||
|
||||
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownListMetaStructure());
|
||||
obj->putDirectOffset(vm, 0, jsBoolean(ordered));
|
||||
obj->putDirectOffset(vm, 1, JSValue::decode(start));
|
||||
obj->putDirectOffset(vm, 2, jsNumber(depth));
|
||||
|
||||
return JSValue::encode(obj);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createCell(
|
||||
JSGlobalObject* globalObject,
|
||||
EncodedJSValue align)
|
||||
{
|
||||
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
|
||||
VM& vm = global->vm();
|
||||
|
||||
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownCellMetaStructure());
|
||||
obj->putDirectOffset(vm, 0, JSValue::decode(align));
|
||||
|
||||
return JSValue::encode(obj);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createLink(
|
||||
JSGlobalObject* globalObject,
|
||||
EncodedJSValue href,
|
||||
EncodedJSValue title)
|
||||
{
|
||||
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
|
||||
VM& vm = global->vm();
|
||||
|
||||
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownLinkMetaStructure());
|
||||
obj->putDirectOffset(vm, 0, JSValue::decode(href));
|
||||
obj->putDirectOffset(vm, 1, JSValue::decode(title));
|
||||
|
||||
return JSValue::encode(obj);
|
||||
}
|
||||
61
src/bun.js/bindings/BunMarkdownMeta.h
Normal file
61
src/bun.js/bindings/BunMarkdownMeta.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include "root.h"
|
||||
#include "headers.h"
|
||||
#include "JavaScriptCore/JSObjectInlines.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
|
||||
using namespace JSC;
|
||||
|
||||
namespace Bun {
|
||||
namespace MarkdownMeta {
|
||||
|
||||
// Cached Structures for the small metadata objects passed as the second
|
||||
// argument to Bun.markdown.render() callbacks. These have fixed shapes
|
||||
// so JSC's property access inline caches stay monomorphic and we avoid
|
||||
// the string-hash + property-transition cost of `put()`-style construction
|
||||
// on every callback (which matters a lot for list items and table cells).
|
||||
|
||||
Structure* createListItemMetaStructure(VM& vm, JSGlobalObject* globalObject);
|
||||
Structure* createListMetaStructure(VM& vm, JSGlobalObject* globalObject);
|
||||
Structure* createCellMetaStructure(VM& vm, JSGlobalObject* globalObject);
|
||||
Structure* createLinkMetaStructure(VM& vm, JSGlobalObject* globalObject);
|
||||
|
||||
} // namespace MarkdownMeta
|
||||
} // namespace Bun
|
||||
|
||||
// ListItemMeta: {index, depth, ordered, start, checked}
|
||||
// `start` and `checked` are always present (jsUndefined() when not applicable)
|
||||
// so the shape is fixed.
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createListItem(
|
||||
JSGlobalObject* globalObject,
|
||||
uint32_t index,
|
||||
uint32_t depth,
|
||||
bool ordered,
|
||||
EncodedJSValue start, // jsNumber or jsUndefined
|
||||
EncodedJSValue checked // jsBoolean or jsUndefined
|
||||
);
|
||||
|
||||
// ListMeta: {ordered, start, depth}
|
||||
// `start` is always present (jsUndefined for unordered).
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createList(
|
||||
JSGlobalObject* globalObject,
|
||||
bool ordered,
|
||||
EncodedJSValue start, // jsNumber or jsUndefined
|
||||
uint32_t depth);
|
||||
|
||||
// CellMeta: {align}
|
||||
// `align` is always present (jsUndefined when no alignment).
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createCell(
|
||||
JSGlobalObject* globalObject,
|
||||
EncodedJSValue align // jsString or jsUndefined
|
||||
);
|
||||
|
||||
// LinkMeta / ImageMeta: {href, title} or {src, title}
|
||||
// `title` is always present (jsUndefined when missing). `href` and `src`
|
||||
// share the structure slot (first property) — the property name differs
|
||||
// but the shape is the same; two separate structures are used.
|
||||
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createLink(
|
||||
JSGlobalObject* globalObject,
|
||||
EncodedJSValue href,
|
||||
EncodedJSValue title // jsString or jsUndefined
|
||||
);
|
||||
@@ -124,6 +124,7 @@
|
||||
#include "JSSink.h"
|
||||
#include "JSSocketAddressDTO.h"
|
||||
#include "JSReactElement.h"
|
||||
#include "BunMarkdownMeta.h"
|
||||
#include "JSSQLStatement.h"
|
||||
#include "JSStringDecoder.h"
|
||||
#include "JSTextEncoder.h"
|
||||
@@ -1802,6 +1803,23 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.set(Bun::JSReactElement::createStructure(init.vm, init.owner));
|
||||
});
|
||||
|
||||
m_JSMarkdownListItemMetaStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::MarkdownMeta::createListItemMetaStructure(init.vm, init.owner));
|
||||
});
|
||||
m_JSMarkdownListMetaStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::MarkdownMeta::createListMetaStructure(init.vm, init.owner));
|
||||
});
|
||||
m_JSMarkdownCellMetaStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::MarkdownMeta::createCellMetaStructure(init.vm, init.owner));
|
||||
});
|
||||
m_JSMarkdownLinkMetaStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::MarkdownMeta::createLinkMetaStructure(init.vm, init.owner));
|
||||
});
|
||||
|
||||
m_JSSQLStatementStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(WebCore::createJSSQLStatementStructure(init.owner));
|
||||
|
||||
@@ -302,6 +302,10 @@ public:
|
||||
Structure* CommonJSModuleObjectStructure() const { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); }
|
||||
Structure* JSSocketAddressDTOStructure() const { return m_JSSocketAddressDTOStructure.getInitializedOnMainThread(this); }
|
||||
Structure* JSReactElementStructure() const { return m_JSReactElementStructure.getInitializedOnMainThread(this); }
|
||||
Structure* JSMarkdownListItemMetaStructure() const { return m_JSMarkdownListItemMetaStructure.getInitializedOnMainThread(this); }
|
||||
Structure* JSMarkdownListMetaStructure() const { return m_JSMarkdownListMetaStructure.getInitializedOnMainThread(this); }
|
||||
Structure* JSMarkdownCellMetaStructure() const { return m_JSMarkdownCellMetaStructure.getInitializedOnMainThread(this); }
|
||||
Structure* JSMarkdownLinkMetaStructure() const { return m_JSMarkdownLinkMetaStructure.getInitializedOnMainThread(this); }
|
||||
Structure* ImportMetaObjectStructure() const { return m_importMetaObjectStructure.getInitializedOnMainThread(this); }
|
||||
Structure* ImportMetaBakeObjectStructure() const { return m_importMetaBakeObjectStructure.getInitializedOnMainThread(this); }
|
||||
Structure* AsyncContextFrameStructure() const { return m_asyncBoundFunctionStructure.getInitializedOnMainThread(this); }
|
||||
@@ -597,6 +601,10 @@ public:
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_commonJSModuleObjectStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSSocketAddressDTOStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSReactElementStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownListItemMetaStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownListMetaStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownCellMetaStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownLinkMetaStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_memoryFootprintStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<JSObject>, m_requireFunctionUnbound) \
|
||||
V(private, LazyPropertyOfGlobalObject<JSObject>, m_requireResolveFunctionUnbound) \
|
||||
|
||||
@@ -138,7 +138,7 @@ pub fn doReadFile(this: *Blob, comptime Function: anytype, global: *JSGlobalObje
|
||||
promise_value.ensureStillAlive();
|
||||
handler.promise.strong.set(global, promise_value);
|
||||
|
||||
read_file.ReadFileUV.start(handler.globalThis.bunVM().eventLoop(), this.store.?, this.offset, this.size, Handler, handler);
|
||||
read_file.ReadFileUV.start(handler.globalThis.bunVM().uvLoop(), this.store.?, this.offset, this.size, Handler, handler);
|
||||
|
||||
return promise_value;
|
||||
}
|
||||
@@ -180,7 +180,7 @@ pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: any
|
||||
pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void {
|
||||
if (Environment.isWindows) {
|
||||
const ReadFileHandler = NewInternalReadFileHandler(Handler, Function);
|
||||
return read_file.ReadFileUV.start(global.bunVM().eventLoop(), this.store.?, this.offset, this.size, ReadFileHandler, ctx);
|
||||
return read_file.ReadFileUV.start(libuv.Loop.get(), this.store.?, this.offset, this.size, ReadFileHandler, ctx);
|
||||
}
|
||||
const file_read = read_file.ReadFile.createWithCtx(
|
||||
bun.default_allocator,
|
||||
|
||||
@@ -523,7 +523,6 @@ pub const ReadFileUV = struct {
|
||||
pub const doClose = FileCloser(@This()).doClose;
|
||||
|
||||
loop: *libuv.Loop,
|
||||
event_loop: *jsc.EventLoop,
|
||||
file_store: FileStore,
|
||||
byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator },
|
||||
store: *Store,
|
||||
@@ -544,11 +543,10 @@ pub const ReadFileUV = struct {
|
||||
|
||||
req: libuv.fs_t = std.mem.zeroes(libuv.fs_t),
|
||||
|
||||
pub fn start(event_loop: *jsc.EventLoop, store: *Store, off: SizeType, max_len: SizeType, comptime Handler: type, handler: *anyopaque) void {
|
||||
pub fn start(loop: *libuv.Loop, store: *Store, off: SizeType, max_len: SizeType, comptime Handler: type, handler: *anyopaque) void {
|
||||
log("ReadFileUV.start", .{});
|
||||
var this = bun.new(ReadFileUV, .{
|
||||
.loop = event_loop.virtual_machine.uvLoop(),
|
||||
.event_loop = event_loop,
|
||||
.loop = loop,
|
||||
.file_store = store.data.file,
|
||||
.store = store,
|
||||
.offset = off,
|
||||
@@ -557,20 +555,15 @@ pub const ReadFileUV = struct {
|
||||
.on_complete_fn = @ptrCast(&Handler.run),
|
||||
});
|
||||
store.ref();
|
||||
// Keep the event loop alive while the async operation is pending
|
||||
event_loop.refConcurrently();
|
||||
this.getFd(onFileOpen);
|
||||
}
|
||||
|
||||
pub fn finalize(this: *ReadFileUV) void {
|
||||
log("ReadFileUV.finalize", .{});
|
||||
const event_loop = this.event_loop;
|
||||
defer {
|
||||
this.store.deref();
|
||||
this.req.deinit();
|
||||
bun.destroy(this);
|
||||
// Release the event loop reference now that we're done
|
||||
event_loop.unrefConcurrently();
|
||||
log("ReadFileUV.finalize destroy", .{});
|
||||
}
|
||||
|
||||
|
||||
@@ -270,14 +270,14 @@ pub const Mask = struct {
|
||||
|
||||
while (true) {
|
||||
if (image == null) {
|
||||
if (input.tryParse(Image.parse, .{}).asValue()) |value| {
|
||||
if (@call(.auto, @field(Image, "parse"), .{input}).asValue()) |value| {
|
||||
image = value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (position == null) {
|
||||
if (input.tryParse(Position.parse, .{}).asValue()) |value| {
|
||||
if (Position.parse(input).asValue()) |value| {
|
||||
position = value;
|
||||
size = input.tryParse(struct {
|
||||
pub inline fn parseFn(i: *css.Parser) css.Result(BackgroundSize) {
|
||||
@@ -290,35 +290,35 @@ pub const Mask = struct {
|
||||
}
|
||||
|
||||
if (repeat == null) {
|
||||
if (input.tryParse(BackgroundRepeat.parse, .{}).asValue()) |value| {
|
||||
if (BackgroundRepeat.parse(input).asValue()) |value| {
|
||||
repeat = value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (origin == null) {
|
||||
if (input.tryParse(GeometryBox.parse, .{}).asValue()) |value| {
|
||||
if (GeometryBox.parse(input).asValue()) |value| {
|
||||
origin = value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (clip == null) {
|
||||
if (input.tryParse(MaskClip.parse, .{}).asValue()) |value| {
|
||||
if (MaskClip.parse(input).asValue()) |value| {
|
||||
clip = value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (composite == null) {
|
||||
if (input.tryParse(MaskComposite.parse, .{}).asValue()) |value| {
|
||||
if (MaskComposite.parse(input).asValue()) |value| {
|
||||
composite = value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == null) {
|
||||
if (input.tryParse(MaskMode.parse, .{}).asValue()) |value| {
|
||||
if (MaskMode.parse(input).asValue()) |value| {
|
||||
mode = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ pub const RedisError = error{
|
||||
UnsupportedProtocol,
|
||||
ConnectionTimeout,
|
||||
IdleTimeout,
|
||||
NestingDepthExceeded,
|
||||
};
|
||||
|
||||
pub fn valkeyErrorToJS(globalObject: *jsc.JSGlobalObject, message: ?[]const u8, err: RedisError) jsc.JSValue {
|
||||
@@ -56,7 +55,6 @@ pub fn valkeyErrorToJS(globalObject: *jsc.JSGlobalObject, message: ?[]const u8,
|
||||
error.InvalidResponseType => .REDIS_INVALID_RESPONSE_TYPE,
|
||||
error.ConnectionTimeout => .REDIS_CONNECTION_TIMEOUT,
|
||||
error.IdleTimeout => .REDIS_IDLE_TIMEOUT,
|
||||
error.NestingDepthExceeded => .REDIS_INVALID_RESPONSE,
|
||||
error.JSError => return globalObject.takeException(error.JSError),
|
||||
error.OutOfMemory => globalObject.throwOutOfMemory() catch return globalObject.takeException(error.JSError),
|
||||
error.JSTerminated => return globalObject.takeException(error.JSTerminated),
|
||||
@@ -422,16 +420,7 @@ pub const ValkeyReader = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Maximum allowed nesting depth for RESP aggregate types.
|
||||
/// This limits recursion to prevent excessive stack usage from
|
||||
/// deeply nested responses.
|
||||
const max_nesting_depth = 128;
|
||||
|
||||
pub fn readValue(self: *ValkeyReader, allocator: std.mem.Allocator) RedisError!RESPValue {
|
||||
return self.readValueWithDepth(allocator, 0);
|
||||
}
|
||||
|
||||
fn readValueWithDepth(self: *ValkeyReader, allocator: std.mem.Allocator, depth: usize) RedisError!RESPValue {
|
||||
const type_byte = try self.readByte();
|
||||
|
||||
return switch (RESPType.fromByte(type_byte) orelse return error.InvalidResponseType) {
|
||||
@@ -462,7 +451,6 @@ pub const ValkeyReader = struct {
|
||||
return RESPValue{ .BulkString = owned };
|
||||
},
|
||||
.Array => {
|
||||
if (depth >= max_nesting_depth) return error.NestingDepthExceeded;
|
||||
const len = try self.readInteger();
|
||||
if (len < 0) return RESPValue{ .Array = &[_]RESPValue{} };
|
||||
const array = try allocator.alloc(RESPValue, @as(usize, @intCast(len)));
|
||||
@@ -474,7 +462,7 @@ pub const ValkeyReader = struct {
|
||||
}
|
||||
}
|
||||
while (i < len) : (i += 1) {
|
||||
array[i] = try self.readValueWithDepth(allocator, depth + 1);
|
||||
array[i] = try self.readValue(allocator);
|
||||
}
|
||||
return RESPValue{ .Array = array };
|
||||
},
|
||||
@@ -507,7 +495,6 @@ pub const ValkeyReader = struct {
|
||||
return RESPValue{ .VerbatimString = try self.readVerbatimString(allocator) };
|
||||
},
|
||||
.Map => {
|
||||
if (depth >= max_nesting_depth) return error.NestingDepthExceeded;
|
||||
const len = try self.readInteger();
|
||||
if (len < 0) return error.InvalidMap;
|
||||
|
||||
@@ -521,15 +508,11 @@ pub const ValkeyReader = struct {
|
||||
}
|
||||
|
||||
while (i < len) : (i += 1) {
|
||||
var key = try self.readValueWithDepth(allocator, depth + 1);
|
||||
errdefer key.deinit(allocator);
|
||||
const value = try self.readValueWithDepth(allocator, depth + 1);
|
||||
entries[i] = .{ .key = key, .value = value };
|
||||
entries[i] = .{ .key = try self.readValue(allocator), .value = try self.readValue(allocator) };
|
||||
}
|
||||
return RESPValue{ .Map = entries };
|
||||
},
|
||||
.Set => {
|
||||
if (depth >= max_nesting_depth) return error.NestingDepthExceeded;
|
||||
const len = try self.readInteger();
|
||||
if (len < 0) return error.InvalidSet;
|
||||
|
||||
@@ -542,12 +525,11 @@ pub const ValkeyReader = struct {
|
||||
}
|
||||
}
|
||||
while (i < len) : (i += 1) {
|
||||
set[i] = try self.readValueWithDepth(allocator, depth + 1);
|
||||
set[i] = try self.readValue(allocator);
|
||||
}
|
||||
return RESPValue{ .Set = set };
|
||||
},
|
||||
.Attribute => {
|
||||
if (depth >= max_nesting_depth) return error.NestingDepthExceeded;
|
||||
const len = try self.readInteger();
|
||||
if (len < 0) return error.InvalidAttribute;
|
||||
|
||||
@@ -560,9 +542,9 @@ pub const ValkeyReader = struct {
|
||||
}
|
||||
}
|
||||
while (i < len) : (i += 1) {
|
||||
var key = try self.readValueWithDepth(allocator, depth + 1);
|
||||
var key = try self.readValue(allocator);
|
||||
errdefer key.deinit(allocator);
|
||||
const value = try self.readValueWithDepth(allocator, depth + 1);
|
||||
const value = try self.readValue(allocator);
|
||||
attrs[i] = .{ .key = key, .value = value };
|
||||
}
|
||||
|
||||
@@ -571,7 +553,7 @@ pub const ValkeyReader = struct {
|
||||
errdefer {
|
||||
allocator.destroy(value_ptr);
|
||||
}
|
||||
value_ptr.* = try self.readValueWithDepth(allocator, depth + 1);
|
||||
value_ptr.* = try self.readValue(allocator);
|
||||
|
||||
return RESPValue{ .Attribute = .{
|
||||
.attributes = attrs,
|
||||
@@ -579,13 +561,11 @@ pub const ValkeyReader = struct {
|
||||
} };
|
||||
},
|
||||
.Push => {
|
||||
if (depth >= max_nesting_depth) return error.NestingDepthExceeded;
|
||||
const len = try self.readInteger();
|
||||
if (len < 0 or len == 0) return error.InvalidPush;
|
||||
|
||||
// First element is the push type
|
||||
var push_type = try self.readValueWithDepth(allocator, depth + 1);
|
||||
defer push_type.deinit(allocator);
|
||||
const push_type = try self.readValue(allocator);
|
||||
var push_type_str: []const u8 = "";
|
||||
|
||||
switch (push_type) {
|
||||
@@ -614,7 +594,7 @@ pub const ValkeyReader = struct {
|
||||
}
|
||||
}
|
||||
while (i < len - 1) : (i += 1) {
|
||||
data[i] = try self.readValueWithDepth(allocator, depth + 1);
|
||||
data[i] = try self.readValue(allocator);
|
||||
}
|
||||
|
||||
return RESPValue{ .Push = .{
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { describe, expect } from "bun:test";
|
||||
import { itBundled } from "../expectBundled";
|
||||
|
||||
describe("css", () => {
|
||||
itBundled("css/mask-geometry-box-preserved", {
|
||||
files: {
|
||||
"index.css": /* css */ `
|
||||
.test-a::after {
|
||||
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
||||
}
|
||||
.test-b::after {
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
}
|
||||
`,
|
||||
},
|
||||
outdir: "/out",
|
||||
entryPoints: ["/index.css"],
|
||||
onAfterBundle(api) {
|
||||
const output = api.readFile("/out/index.css");
|
||||
expect(output).toContain("padding-box");
|
||||
expect(output).toContain("content-box");
|
||||
expect(output).toContain(".test-a");
|
||||
expect(output).toContain(".test-b");
|
||||
expect(output).not.toContain(".test-a:after, .test-b:after");
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("css/webkit-mask-geometry-box-preserved", {
|
||||
files: {
|
||||
"index.css": /* css */ `
|
||||
.test-c::after {
|
||||
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
}
|
||||
`,
|
||||
},
|
||||
outdir: "/out",
|
||||
entryPoints: ["/index.css"],
|
||||
onAfterBundle(api) {
|
||||
const output = api.readFile("/out/index.css");
|
||||
expect(output).toContain("padding-box");
|
||||
},
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user