Public: anyone can read this; the crew can change it.

A .dxf drawing opens as a picture on its file page: lines, circles, arcs and polylines #11913

opened this

Check · Review · Land · Live — No review yet. Next: reviewer, no review yet.

Ready to land

torque/forge:torque/11861-a-dxf-drawing-opens-as-a-picture-on-its → main 2 commits fork

Review

Sign in to review this change.
8 files changed +353 −1
docs/design/areas/render.md +4
@@ -32,6 +32,10 @@ A .zip, .tar or .tar.gz is drawn by `src/web/render/archive-view.ts` as the exis
3232
3333 `src/web/render/geojson.ts` draws a GeoJSON file as one inline SVG in `.gj-view` (same rules as `.gpx-view`: no fixed size, `max-width: 100%`, `currentColor`), and the first 100 features' properties as a plain table in `.gj-table`, which scrolls sideways inside its own box on a phone.
3434
35 ## Drawing (.dxf)
36
37 `src/web/render/dxf.ts` draws an ASCII DXF as one inline SVG in `.dxf-view` (same rules as `.gpx-view`: no fixed size, `max-width: 100%`, `currentColor`, no accent), with its entity counts and layer names as the existing `.status-rows`.
38
3539 ## Request waterfall (.har)
3640
3741 `src/web/render/har.ts` draws a HAR as the existing `.table` (`data-dense`) inside `.har-view`, which scrolls sideways on a phone. Each row's timing bar is one inline SVG, `.har-url` (the URL cell, wraps anywhere) and `.har-bar` (the full width of the URL cell, under the URL, 10px tall, `--muted-fg` (it is data, not a control, so not the accent), `aria-hidden` since the Time column says the same in words, no inline style). `.har-url` has `min-width: 200px` so a row never squeezes to a sliver; that cannot make the page scroll, because `.har-view` is `overflow-x: auto`, so any excess width scrolls inside the table's own box, not the page. Headers sit in a native `<details>` under the URL. How it works: `docs/features/file-har.md`.
src/web/render/dxf.ts added +236
@@ -0,0 +1,236 @@
1 /**
2 * An ASCII DXF drawing (`.dxf`, AutoCAD's exchange format) drawn as inline SVG.
3 * The file is a list of (group code, value) line pairs; this reads the
4 * ENTITIES section and draws LINE, CIRCLE, ARC and LWPOLYLINE, scaled to their
5 * bounding box with the y axis flipped (DXF is y-up, SVG y-down). Angles are
6 * degrees counter-clockwise, as in the DXF reference (group codes 10/20/11/21
7 * points, 40 radius, 50/51 arc angles, 70 polyline flags, 8 layer). Bulges
8 * (code 42) are ignored: a polyline's segments are drawn straight. TEXT and
9 * every other entity are counted as ignored, never drawn. A binary DXF has a
10 * NUL in its head and never reaches here; a file with no entities, no EOF
11 * or no ENTITIES section falls back to the source view. Every number is
12 * finite and clamped, every string escaped.
13 */
14
15 import { html, raw } from "../html.ts";
16 import type { Rendered, RenderInput } from "./renderers.ts";
17 import { RENDER_CAP } from "./kinds.ts";
18 import { fileSize } from "../ui.ts";
19
20 /** Entities drawn in all; the rest are counted but not drawn. */
21 export const ENTITY_CAP = 50_000;
22 const LAYER_SHOWN = 50;
23 const NAME_CHARS = 80;
24 /** Coordinates beyond this magnitude are treated as corrupt. */
25 const LIMIT = 1e9;
26
27 type Pt = readonly [number, number];
28 export type Shape =
29 | { readonly t: "line"; readonly a: Pt; readonly b: Pt }
30 | { readonly t: "circle"; readonly c: Pt; readonly r: number }
31 | { readonly t: "arc"; readonly c: Pt; readonly r: number; readonly from: number; readonly to: number }
32 | { readonly t: "poly"; readonly pts: Pt[]; readonly closed: boolean };
33
34 export interface Drawing {
35 readonly shapes: Shape[];
36 /** Entity counts by type, drawn or not. */
37 readonly counts: Map<string, number>;
38 readonly layers: string[];
39 }
40
41 const num = (s: string | undefined): number | undefined => {
42 if (s === undefined || s.trim() === "") return undefined;
43 const n = Number(s);
44 return Number.isFinite(n) && Math.abs(n) <= LIMIT ? n : undefined;
45 };
46
47 /** Parse DXF text. Pure. An error string when it cannot be drawn. */
48 export function parseDxf(text: string): Drawing | { error: string } {
49 const lines = text.replace(/^/, "").split(/\r\n|\n|\r/);
50 if (lines.length < 4) return { error: "it is too short to be a drawing" };
51 let inEntities = false;
52 let sawEntities = false;
53 let sawEof = false;
54 const counts = new Map<string, number>();
55 const layers = new Set<string>();
56 const shapes: Shape[] = [];
57 type Cur = { type: string; layer: string; codes: [number, string][] };
58 let cur: Cur | undefined;
59 let expectName = false;
60
61 const finish = (): void => {
62 if (cur === undefined) return;
63 const e = cur;
64 cur = undefined;
65 counts.set(e.type, (counts.get(e.type) ?? 0) + 1);
66 layers.add(e.layer);
67 if (shapes.length >= ENTITY_CAP) return;
68 const first = (code: number): number | undefined => num(e.codes.find((c) => c[0] === code)?.[1]);
69 const x = first(10), y = first(20);
70 switch (e.type) {
71 case "LINE": {
72 const x2 = first(11), y2 = first(21);
73 if (x !== undefined && y !== undefined && x2 !== undefined && y2 !== undefined)
74 shapes.push({ t: "line", a: [x, y], b: [x2, y2] });
75 break;
76 }
77 case "CIRCLE": {
78 const r = first(40);
79 if (x !== undefined && y !== undefined && r !== undefined && r > 0) shapes.push({ t: "circle", c: [x, y], r });
80 break;
81 }
82 case "ARC": {
83 const r = first(40), from = first(50), to = first(51);
84 if (x !== undefined && y !== undefined && r !== undefined && r > 0 && from !== undefined && to !== undefined)
85 shapes.push({ t: "arc", c: [x, y], r, from, to });
86 break;
87 }
88 case "LWPOLYLINE": {
89 const pts: Pt[] = [];
90 let px: number | undefined;
91 for (const [code, v] of e.codes) {
92 if (code === 10) px = num(v);
93 else if (code === 20 && px !== undefined) {
94 const py = num(v);
95 if (py !== undefined) pts.push([px, py]);
96 px = undefined;
97 }
98 }
99 const flags = first(70) ?? 0;
100 if (pts.length >= 2) shapes.push({ t: "poly", pts, closed: (flags & 1) === 1 });
101 break;
102 }
103 }
104 };
105
106 for (let i = 0; i + 1 < lines.length; i += 2) {
107 const code = Number(lines[i]!.trim());
108 const value = lines[i + 1]!.trim();
109 if (!Number.isInteger(code)) return { error: "it is not group-code, value pairs" };
110 if (code === 0) {
111 finish();
112 if (value === "EOF") {
113 sawEof = true;
114 break;
115 }
116 if (value === "SECTION") {
117 expectName = true;
118 inEntities = false;
119 } else if (value === "ENDSEC") inEntities = false;
120 else if (inEntities) cur = { type: value.slice(0, NAME_CHARS), layer: "0", codes: [] };
121 continue;
122 }
123 if (expectName && code === 2) {
124 expectName = false;
125 inEntities = value === "ENTITIES";
126 if (inEntities) sawEntities = true;
127 continue;
128 }
129 if (cur === undefined) continue;
130 if (code === 8) cur.layer = value.slice(0, NAME_CHARS);
131 else if (cur.codes.length < 100_000) cur.codes.push([code, value]);
132 }
133 if (!sawEntities) return { error: "it has no ENTITIES section" };
134 if (!sawEof) return { error: "it is cut short (no EOF)" };
135 if (shapes.length === 0) return { error: "it has no line, circle, arc or polyline to draw" };
136 return { shapes, counts, layers: [...layers].sort() };
137 }
138
139 const W = 640;
140 const MAX_H = 480;
141 const PAD = 12;
142 const rad = (d: number): number => (d * Math.PI) / 180;
143 const norm360 = (d: number): number => ((d % 360) + 360) % 360;
144
145 /** Corner points of a shape's extent, for the bounding box. */
146 function extent(s: Shape): Pt[] {
147 switch (s.t) {
148 case "line":
149 return [s.a, s.b];
150 case "poly":
151 return s.pts;
152 case "circle":
153 return [[s.c[0] - s.r, s.c[1] - s.r], [s.c[0] + s.r, s.c[1] + s.r]];
154 case "arc": {
155 const span = norm360(s.to - s.from) || 360;
156 const out: Pt[] = [];
157 const at = (deg: number): void => void out.push([s.c[0] + s.r * Math.cos(rad(deg)), s.c[1] + s.r * Math.sin(rad(deg))]);
158 at(s.from);
159 at(s.from + span);
160 for (const q of [0, 90, 180, 270]) if (norm360(q - s.from) <= span) at(q);
161 return out;
162 }
163 }
164 }
165
166 /** The SVG body and its size, fitted to the shapes' bounds. */
167 export function draw(shapes: readonly Shape[]): { width: number; height: number; body: string; lines: number; rounds: number } {
168 let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
169 for (const s of shapes)
170 for (const p of extent(s)) {
171 minX = Math.min(minX, p[0]);
172 maxX = Math.max(maxX, p[0]);
173 minY = Math.min(minY, p[1]);
174 maxY = Math.max(maxY, p[1]);
175 }
176 const spanX = Math.max(maxX - minX, 1e-9);
177 const spanY = Math.max(maxY - minY, 1e-9);
178 let scale = (W - 2 * PAD) / spanX;
179 let height = spanY * scale + 2 * PAD;
180 if (height > MAX_H) {
181 scale = (MAX_H - 2 * PAD) / spanY;
182 height = MAX_H;
183 }
184 const width = Math.max(spanX * scale + 2 * PAD, 2 * PAD + 1);
185 const f = (n: number): string => (Number.isFinite(n) ? n : 0).toFixed(2);
186 const x = (v: number): string => f(PAD + (v - minX) * scale);
187 const y = (v: number): string => f(PAD + (maxY - v) * scale);
188 let d = "", circles = "", lines = 0, rounds = 0;
189 for (const s of shapes) {
190 if (s.t === "line") {
191 d += `M${x(s.a[0])} ${y(s.a[1])}L${x(s.b[0])} ${y(s.b[1])}`;
192 lines++;
193 } else if (s.t === "poly") {
194 d += s.pts.map((p, i) => `${i === 0 ? "M" : "L"}${x(p[0])} ${y(p[1])}`).join("") + (s.closed ? "Z" : "");
195 lines++;
196 } else if (s.t === "circle") {
197 circles += `<circle cx="${x(s.c[0])}" cy="${y(s.c[1])}" r="${f(s.r * scale)}"/>`;
198 rounds++;
199 } else {
200 const span = norm360(s.to - s.from);
201 const r = f(s.r * scale);
202 const p = (deg: number): string => `${x(s.c[0] + s.r * Math.cos(rad(deg)))} ${y(s.c[1] + s.r * Math.sin(rad(deg)))}`;
203 if (span === 0) circles += `<circle cx="${x(s.c[0])}" cy="${y(s.c[1])}" r="${r}"/>`;
204 else d += `M${p(s.from)}A${r} ${r} 0 ${span > 180 ? 1 : 0} 0 ${p(s.from + span)}`;
205 rounds++;
206 }
207 }
208 const body = (d === "" ? "" : `<path d="${d}"/>`) + circles;
209 return { width: Math.round(width), height: Math.round(height), body, lines, rounds };
210 }
211
212 const plural = (n: number, one: string): string => `${n.toLocaleString("en")} ${one}${n === 1 ? "" : "s"}`;
213
214 export function dxf(input: RenderInput): Rendered {
215 const fall = (why: string): Rendered => ({ content: html``, lines: 0, fallback: `This file is shown as text because ${why}.` });
216 if (input.size > RENDER_CAP.dxf || input.body.length > RENDER_CAP.dxf) return fall("it is too large to draw");
217 const parsed = parseDxf(new TextDecoder().decode(input.body));
218 if ("error" in parsed) return fall(parsed.error);
219 const drawn = draw(parsed.shapes);
220 const entries = [...parsed.counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
221 const total = entries.reduce((n, e) => n + e[1], 0);
222 const shownLayers = parsed.layers.slice(0, LAYER_SHOWN);
223 const more = parsed.layers.length - shownLayers.length;
224 const facts: [string, string][] = [
225 ["Entities", `${total.toLocaleString("en")} (${entries.map(([t, n]) => `${n.toLocaleString("en")} ${t}`).join(", ")})`],
226 ["Layers", shownLayers.join(", ") + (more > 0 ? ` and ${more} more` : "")],
227 ["Units", "drawing units, as stored; the file does not say which"],
228 ];
229 const label = `Drawing of ${plural(total, "entity").replace("entitys", "entities")}`;
230 const content = html`<figure class="dxf-view">
231 <svg viewBox="0 0 ${drawn.width} ${drawn.height}" role="img" aria-label="${label}" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none">${raw(drawn.body)}</svg>
232 <p class="muted small">Only LINE, CIRCLE, ARC and LWPOLYLINE are drawn, in one colour; polyline bulges are drawn straight. Other entities are counted, not drawn.${total > ENTITY_CAP ? ` Past ${ENTITY_CAP.toLocaleString("en")} entities nothing more is drawn.` : ""}</p>
233 <dl class="status-rows">${facts.map(([k, v]) => html`<div class="status-row"><dt>${k}</dt><dd>${v}</dd></div>`)}</dl>
234 </figure>`;
235 return { content, lines: 0, summary: `${plural(total, "entity").replace("entitys", "entities")} · ${fileSize(input.size)}` };
236 }
src/web/render/kinds.ts +8 −1
@@ -38,6 +38,7 @@ export type FileKind =
3838 | "contacts"
3939 | "mail"
4040 | "gcode"
41 | "dxf"
4142 | "sarif"
4243 | "lcov"
4344 | "junit"
@@ -86,6 +87,7 @@ export const RENDER_CAP: Readonly<Record<FileKind, number>> = {
8687 contacts: 5 * MB,
8788 mail: 5 * MB,
8889 gcode: 50 * MB,
90 dxf: 5 * MB,
8991 sarif: 5 * MB,
9092 lcov: 5 * MB,
9193 junit: 5 * MB,
@@ -220,6 +222,7 @@ const TEXTUAL: Readonly<Record<string, FileKind>> = {
220222 eml: "mail",
221223 gcode: "gcode",
222224 gco: "gcode",
225 dxf: "dxf",
223226 sarif: "sarif",
224227 lcov: "lcov",
225228 sql: "sql",
@@ -269,6 +272,10 @@ const isArchive = (path: string, head: Uint8Array): boolean => {
269272 return ARCHIVES[ext]?.(head) === true;
270273 };
271274
275 /** An ASCII DXF opens on group code 0, then `SECTION`, whatever the file is named. */
276 const isDxfHead = (head: Uint8Array): boolean =>
277 /^\uFEFF?\s*0\r?\nSECTION\r?\n/.test(new TextDecoder().decode(head.subarray(0, 64)));
278
272279 /** For a caller that has only a name: the kind its extension gives. */
273280 export const NO_BYTES: Uint8Array = new Uint8Array(0);
274281
@@ -325,7 +332,7 @@ export function fileKind(path: string, head: Uint8Array): FileKind {
325332 if (isDockerfileName(path)) return "dockerfile";
326333 if (isWorkflowName(path)) return "workflow";
327334 if (isComposeName(path)) return "compose";
328 return TEXTUAL[extension(path)] ?? "text";
335 return TEXTUAL[extension(path)] ?? (isDxfHead(head) ? "dxf" : "text");
329336 }
330337
331338 /** Kinds whose renderer links paths to files in the repository, so the page reads the ref's file list. */
src/web/render/renderers.ts +2
@@ -16,6 +16,7 @@ import { graphql } from "./graphql.ts";
1616 import { eml } from "./eml.ts";
1717 import { csv } from "./csv.ts";
1818 import { geojson } from "./geojson.ts";
19 import { dxf } from "./dxf.ts";
1920 import { gcode } from "./gcode.ts";
2021 import { gpx } from "./gpx.ts";
2122 import { har } from "./har.ts";
@@ -102,6 +103,7 @@ export const RENDERERS: Partial<Readonly<Record<FileKind, Renderer>>> = {
102103 gpx,
103104 har,
104105 gcode,
106 dxf,
105107 geojson,
106108 calendar: ics,
107109 contacts: vcf,
src/web/style/content.ts +3
@@ -524,6 +524,9 @@ form.inline { display: inline; margin-left: auto; }
524524 .har-view { overflow-x: auto; }
525525 .har-view .har-bar { display: block; width: 100%; height: 10px; margin-top: 4px; color: var(--muted-fg); }
526526 .har-view .har-url { overflow-wrap: anywhere; min-width: 200px; }
527 .dxf-view { margin: 0; }
528 .dxf-view svg { display: block; max-width: 100%; height: auto; margin: 0 auto; color: var(--fg); }
529 .dxf-view .status-rows { margin-top: 12px; }
527530 .gj-view { margin: 0; }
528531 .gj-view svg { display: block; max-width: 100%; height: auto; margin: 0 auto; color: var(--fg); }
529532 .gj-table { overflow-x: auto; max-width: 100%; }
test/fuzz-renderers.test.ts +6
@@ -15,6 +15,7 @@ import { ics } from "../src/web/render/ics.ts";
1515 import { eml } from "../src/web/render/eml.ts";
1616 import { vcf } from "../src/web/render/vcf.ts";
1717 import { gpx } from "../src/web/render/gpx.ts";
18 import { dxf } from "../src/web/render/dxf.ts";
1819 import { geojson } from "../src/web/render/geojson.ts";
1920 import { subtitles } from "../src/web/render/subtitles.ts";
2021 import { patch } from "../src/web/render/patch.ts";
@@ -105,6 +106,11 @@ const SAMPLES: Record<string, { path: string; render: Renderer; files: readonly
105106 '<?xml version="1.0"?><gpx version="1.1" creator="x"><metadata><name>Walk</name><link href="https://a.test"/></metadata><trk><name>T</name><trkseg><trkpt lat="51.5" lon="-0.1"><ele>10</ele><time>2026-01-01T10:00:00Z</time></trkpt><trkpt lat="51.6" lon="-0.2"><ele>12</ele></trkpt></trkseg></trk><wpt lat="51.5" lon="-0.1"><name>W</name><desc>d</desc></wpt></gpx>',
106107 ],
107108 },
109 dxf: {
110 path: "part.dxf",
111 render: dxf,
112 files: ["0\nSECTION\n2\nENTITIES\n0\nLINE\n8\n<script>alert(1)</script>\n10\n0\n20\n0\n11\n5\n21\n5\n0\nCIRCLE\n10\n1\n20\n1\n40\n2\n0\nENDSEC\n0\nEOF\n"],
113 },
108114 geojson: {
109115 path: "map.geojson",
110116 render: geojson,
test/render-dxf.test.ts added +93
@@ -0,0 +1,93 @@
1 /** DXF drawings as inline SVG. */
2 import { describe, expect, test } from "bun:test";
3 import { dxf, parseDxf } from "../src/web/render/dxf.ts";
4 import { fileKind, RENDER_CAP } from "../src/web/render/kinds.ts";
5 import { RENDERERS } from "../src/web/render/renderers.ts";
6
7 const enc = (s: string): Uint8Array => new TextEncoder().encode(s);
8 const pairs = (...p: (string | number)[]): string => p.join("\n") + "\n";
9 const file = (entities: string, eof = true): string =>
10 pairs(0, "SECTION", 2, "HEADER", 0, "ENDSEC", 0, "SECTION", 2, "ENTITIES") + entities + pairs(0, "ENDSEC") + (eof ? pairs(0, "EOF") : "");
11 const draw = (text: string) => dxf({ path: "r/a.dxf", size: text.length, body: enc(text), media: "/m", download: "/d" });
12
13 const ALL = file(
14 pairs(0, "LINE", 8, "Walls", 10, 0, 20, 0, 11, 100, 21, 0) +
15 pairs(0, "CIRCLE", 8, "Holes", 10, 50, 20, 50, 40, 10) +
16 pairs(0, "ARC", 8, "Walls", 10, 0, 20, 0, 40, 20, 50, 0, 51, 90) +
17 pairs(0, "LWPOLYLINE", 8, "Outline", 70, 1, 10, 0, 20, 0, 10, 10, 20, 0, 10, 10, 20, 10) +
18 pairs(0, "TEXT", 8, "Notes", 10, 1, 20, 1, 1, "hello"),
19 );
20
21 describe("dxf", () => {
22 test("registered, by name and by head", () => {
23 expect(fileKind("a.dxf", enc(ALL))).toBe("dxf");
24 expect(fileKind("drawing.txt", enc(ALL))).toBe("dxf");
25 expect(fileKind("notes.txt", enc("hello\n"))).toBe("text");
26 expect(RENDERERS.dxf).toBe(dxf);
27 expect(RENDER_CAP.dxf).toBeGreaterThan(0);
28 });
29
30 test("one of each entity gives its elements and facts", () => {
31 const out = String(draw(ALL).content);
32 expect(out.match(/<svg/g)?.length).toBe(1);
33 expect(out.match(/<circle/g)?.length).toBe(1);
34 expect(out.match(/<path/g)?.length).toBe(1);
35 // line + arc + polyline subpaths: three M commands
36 expect(out.match(/M/g)?.length).toBe(3);
37 expect(out).toContain("A"); // the arc command
38 expect(out).toContain("Z"); // closed polyline
39 expect(out).toContain("5 (1 ARC, 1 CIRCLE, 1 LINE, 1 LWPOLYLINE, 1 TEXT)");
40 expect(out).toContain("Holes, Notes, Outline, Walls");
41 expect(out).not.toMatch(/<svg[^>]*\s(width|height)=/);
42 });
43
44 test("the drawing is scaled to its bounding box, y flipped", () => {
45 const out = String(draw(file(pairs(0, "LINE", 10, 0, 20, 0, 11, 100, 21, 50))).content);
46 expect(out).toContain("M12.00 "); // west edge at the padding
47 expect(out).toContain('viewBox="0 0 640 ');
48 // (0,0) is the bottom: its y is larger than the top end's y (50)
49 const m = /M12\.00 ([\d.]+)L[\d.]+ ([\d.]+)/.exec(out)!;
50 expect(Number(m[1])).toBeGreaterThan(Number(m[2]));
51 });
52
53 test("empty, truncated and header-only files fall back plainly", () => {
54 for (const text of ["", "0\nSECTION\n", file("", true), file(pairs(0, "LINE", 10, 0, 20, 0, 11, 1, 21, 1), false), "hello world\nnot dxf\nat all\nreally\n"]) {
55 const r = draw(text);
56 expect(r.fallback).toBeString();
57 expect(r.fallback!.length).toBeGreaterThan(10);
58 }
59 });
60
61 test("a 10 MB file is refused, not parsed", () => {
62 const body = new Uint8Array(10_000_000).fill(0x30);
63 const r = dxf({ path: "a.dxf", size: body.length, body, media: "/m", download: "/d" });
64 expect(r.fallback).toContain("too large");
65 });
66
67 test("NaN, Infinity and absurd numbers never reach the SVG", () => {
68 const bad = file(
69 pairs(0, "LINE", 10, "NaN", 20, 0, 11, 1, 21, 1) +
70 pairs(0, "LINE", 10, "Infinity", 20, 0, 11, 1, 21, 1) +
71 pairs(0, "CIRCLE", 10, 0, 20, 0, 40, 1e300) +
72 pairs(0, "LINE", 10, 0, 20, 0, 11, 4, 21, 3),
73 );
74 const parsed = parseDxf(bad);
75 if ("error" in parsed) throw new Error(parsed.error);
76 expect(parsed.shapes.length).toBe(1);
77 const out = String(draw(bad).content);
78 expect(out).not.toMatch(/NaN|Infinity|e\+/);
79 });
80
81 test("layer names and entity types are escaped", () => {
82 const out = String(draw(file(pairs(0, "LINE", 8, "<img src=x onerror=alert(1)>", 10, 0, 20, 0, 11, 1, 21, 1) + pairs(0, "<script>", 8, "a"))).content);
83 expect(out).not.toContain("<img");
84 expect(out).not.toContain("<script");
85 expect(out).toContain("&lt;img");
86 });
87
88 test("an unknown entity is counted, not an error", () => {
89 const r = draw(file(pairs(0, "LINE", 10, 0, 20, 0, 11, 1, 21, 1) + pairs(0, "SPLINE", 8, "0")));
90 expect(r.fallback).toBeUndefined();
91 expect(String(r.content)).toContain("1 SPLINE");
92 });
93 });
test/render-kinds.test.ts +1
@@ -81,6 +81,7 @@ const SAMPLES: Record<string, [Uint8Array, FileKind, string]> = {
8181 ics: [enc("BEGIN:VCALENDAR\nEND:VCALENDAR\n"), "calendar", "application/octet-stream"],
8282 gcode: [enc("G28\nG1 X1 E1\n"), "gcode", "application/octet-stream"],
8383 gco: [enc("G28\n"), "gcode", "application/octet-stream"],
84 dxf: [enc("0\nSECTION\n2\nENTITIES\n0\nENDSEC\n0\nEOF\n"), "dxf", "application/octet-stream"],
8485 har: [enc(`{"log":{"entries":[]}}`), "har", "application/octet-stream"],
8586 sql: [enc("CREATE TABLE a (id int);\n"), "sql", "application/octet-stream"],
8687 graphql: [enc("type A { b: Int }\n"), "graphql", "application/octet-stream"],

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs