@marimo-team/islands 0.19.7-dev31 → 0.19.7-dev34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { MimeType } from "@/components/editor/Output";
4
+ import { once } from "./once";
5
+
6
+ /**
7
+ * Configuration for mime type precedence and filtering.
8
+ * Uses Map/Set for O(1) lookups at runtime.
9
+ */
10
+ export interface MimeTypeConfig {
11
+ /**
12
+ * Pre-computed precedence map: mime type -> sort index.
13
+ * Lower index = higher priority. Types not in the map are placed at the end.
14
+ */
15
+ precedence: ReadonlyMap<MimeType, number>;
16
+
17
+ /**
18
+ * Hiding rules: trigger mime type -> set of mime types to hide.
19
+ * When the key mime type is present, all mime types in the value set are hidden.
20
+ */
21
+ hidingRules: ReadonlyMap<MimeType, ReadonlySet<MimeType>>;
22
+ }
23
+
24
+ /**
25
+ * Result of processing mime types through the filtering and sorting pipeline.
26
+ */
27
+ export interface ProcessedMimeTypes<T> {
28
+ /** The filtered and sorted mime entries */
29
+ entries: Array<[MimeType, T]>;
30
+ /** Mime types that were hidden by rules */
31
+ hidden: MimeType[];
32
+ }
33
+
34
+ /**
35
+ * Creates a compiled MimeTypeConfig from readable arrays.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const config = createMimeConfig({
40
+ * precedence: ["text/html", "image/png", "text/plain"],
41
+ * hidingRules: {
42
+ * "text/html": ["image/png", "image/jpeg"],
43
+ * },
44
+ * });
45
+ * ```
46
+ */
47
+ export function createMimeConfig(input: {
48
+ precedence: MimeType[];
49
+ hidingRules: Record<string, MimeType[]>;
50
+ }): MimeTypeConfig {
51
+ const precedence = new Map<MimeType, number>();
52
+ for (let i = 0; i < input.precedence.length; i++) {
53
+ precedence.set(input.precedence[i], i);
54
+ }
55
+
56
+ const hidingRules = new Map<MimeType, ReadonlySet<MimeType>>();
57
+ for (const [trigger, toHide] of Object.entries(input.hidingRules)) {
58
+ hidingRules.set(trigger as MimeType, new Set(toHide));
59
+ }
60
+
61
+ return { precedence, hidingRules };
62
+ }
63
+
64
+ /**
65
+ * Default configuration for mime type handling.
66
+ * Lazily compiled on first access.
67
+ *
68
+ * Design rationale:
69
+ * - text/html typically contains rich rendered output and should take precedence
70
+ * - When text/html is present, image fallbacks (png, jpeg, etc.) are often redundant
71
+ * static renders and should be hidden to reduce UI clutter
72
+ * - text/markdown should NOT be hidden by text/html as they serve different purposes
73
+ * - Vega charts should remain visible as they provide interactivity
74
+ */
75
+ export const getDefaultMimeConfig = once((): MimeTypeConfig => {
76
+ const IMAGE_FALLBACKS: MimeType[] = ["image/png", "image/jpeg", "image/gif"];
77
+
78
+ return createMimeConfig({
79
+ precedence: [
80
+ "text/html",
81
+ "application/vnd.vegalite.v6+json",
82
+ "application/vnd.vegalite.v5+json",
83
+ "application/vnd.vega.v6+json",
84
+ "application/vnd.vega.v5+json",
85
+ "image/svg+xml",
86
+ "image/png",
87
+ "image/jpeg",
88
+ "image/gif",
89
+ "text/markdown",
90
+ "text/latex",
91
+ "text/csv",
92
+ "application/json",
93
+ "text/plain",
94
+ "video/mp4",
95
+ "video/mpeg",
96
+ ],
97
+ hidingRules: {
98
+ // When HTML is present, hide static image fallbacks
99
+ "text/html": [
100
+ ...IMAGE_FALLBACKS,
101
+ "image/avif",
102
+ "image/bmp",
103
+ "image/tiff",
104
+ ],
105
+ // When Vega charts are present, hide image fallbacks
106
+ "application/vnd.vegalite.v6+json": IMAGE_FALLBACKS,
107
+ "application/vnd.vegalite.v5+json": IMAGE_FALLBACKS,
108
+ "application/vnd.vega.v6+json": IMAGE_FALLBACKS,
109
+ "application/vnd.vega.v5+json": IMAGE_FALLBACKS,
110
+ },
111
+ });
112
+ });
113
+
114
+ /**
115
+ * Filters mime types based on hiding rules.
116
+ */
117
+ export function applyHidingRules(
118
+ mimeTypes: ReadonlySet<MimeType>,
119
+ rules: ReadonlyMap<MimeType, ReadonlySet<MimeType>>,
120
+ ): { visible: Set<MimeType>; hidden: Set<MimeType> } {
121
+ const hidden = new Set<MimeType>();
122
+
123
+ for (const mime of mimeTypes) {
124
+ const toHide = rules.get(mime);
125
+ if (toHide) {
126
+ for (const hideType of toHide) {
127
+ if (mimeTypes.has(hideType)) {
128
+ hidden.add(hideType);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ const visible = new Set<MimeType>();
135
+ for (const mime of mimeTypes) {
136
+ if (!hidden.has(mime)) {
137
+ visible.add(mime);
138
+ }
139
+ }
140
+
141
+ return { visible, hidden };
142
+ }
143
+
144
+ /**
145
+ * Sorts mime entries according to a precedence map.
146
+ * Mime types not in the map are placed at the end, preserving their original order.
147
+ */
148
+ export function sortByPrecedence<T>(
149
+ entries: Array<[MimeType, T]>,
150
+ precedence: ReadonlyMap<MimeType, number>,
151
+ ): Array<[MimeType, T]> {
152
+ const unknownPrecedence = precedence.size;
153
+
154
+ return [...entries].sort((a, b) => {
155
+ const indexA = precedence.get(a[0]) ?? unknownPrecedence;
156
+ const indexB = precedence.get(b[0]) ?? unknownPrecedence;
157
+ return indexA - indexB;
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Main entry point: processes mime entries by applying hiding rules and sorting.
163
+ */
164
+ export function processMimeBundle<T>(
165
+ entries: Array<[MimeType, T]>,
166
+ config: MimeTypeConfig = getDefaultMimeConfig(),
167
+ ): ProcessedMimeTypes<T> {
168
+ if (entries.length === 0) {
169
+ return { entries: [], hidden: [] };
170
+ }
171
+
172
+ const mimeTypes = new Set(entries.map(([mime]) => mime));
173
+ const { visible, hidden } = applyHidingRules(mimeTypes, config.hidingRules);
174
+ const filteredEntries = entries.filter(([mime]) => visible.has(mime));
175
+ const sortedEntries = sortByPrecedence(filteredEntries, config.precedence);
176
+
177
+ return {
178
+ entries: sortedEntries,
179
+ hidden: Array.from(hidden),
180
+ };
181
+ }