Compare commits

..

3 Commits

Author SHA1 Message Date
Jarred Sumner
65bb2c68ed docs: shorten listItem table row, break fields out to bullets 2026-03-02 04:02:46 -08:00
autofix-ci[bot]
cad86ad550 [autofix.ci] apply automated fixes 2026-03-02 12:02:11 +00:00
Jarred Sumner
a87b8e10ad markdown: add {index, depth, ordered, start} to listItem callback meta
The render() callback API is bottom-up (exit-only), so listItem callbacks
previously couldn't know their index within the parent list, whether the
parent was ordered, or their nesting depth — forcing consumers to do
string-sentinel workarounds and post-process the output with regex.

This plumbs through the info the parser already has:
- listItem meta: {index, depth, ordered, start, checked}
- list meta: {ordered, start, depth}

Implementation:
- Track block_type + child_index in JsCallbackRenderer.StackEntry
- Compute depth by walking stack for ul/ol ancestors in createBlockMeta
- Parent list's ordered/start read from stack[len-2]

Also adds BunMarkdownMeta.{cpp,h} with cached JSC Structures for the
high-frequency meta objects (listItem, list, th/td, link) — putDirectOffset
instead of N×put() keeps them monomorphic and essentially free (~0.7ns/obj).
2026-03-02 04:00:16 -08:00
151 changed files with 1372 additions and 1356 deletions

View File

@@ -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 16. `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 16. `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

View File

@@ -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. */

View File

@@ -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;
}

View File

@@ -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));
}

View 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);
}

View 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
);

View File

@@ -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));

View File

@@ -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) \

View File

@@ -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,

View File

@@ -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", .{});
}

View File

@@ -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;
}

View File

@@ -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 = .{

View File

@@ -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