From fd6104e7c853bee97c878690c1c66d156c0f981f Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 25 Dec 2025 09:52:26 -0600 Subject: [PATCH] feat: markdown data model * export md as svg (foreignObject) * rendering of md --- packages/core/src/export.ts | 35 +- packages/core/src/geom.ts | 46 ++ packages/core/src/model.ts | 47 +- packages/core/src/reactivity.ts | 2 +- packages/core/src/tools/index.ts | 1 + packages/core/src/tools/markdown.ts | 66 +++ packages/core/src/tools/select.ts | 12 +- packages/core/tests/markdown.test.ts | 394 ++++++++++++++ packages/renderer/package.json | 9 +- packages/renderer/src/index.ts | 223 +++++++- pnpm-lock.yaml | 737 +++++++++++++++++++++++++++ 11 files changed, 1563 insertions(+), 9 deletions(-) create mode 100644 packages/core/src/tools/markdown.ts create mode 100644 packages/core/tests/markdown.test.ts diff --git a/packages/core/src/export.ts b/packages/core/src/export.ts index 2dcd861..7da89cd 100644 --- a/packages/core/src/export.ts +++ b/packages/core/src/export.ts @@ -1,7 +1,7 @@ import { shapeBounds } from "./geom"; import type { Box2 } from "./math"; import { Box2 as Box2Ops } from "./math"; -import type { ArrowShape, EllipseShape, LineShape, RectShape, ShapeRecord, TextShape } from "./model"; +import type { ArrowShape, EllipseShape, LineShape, MarkdownShape, RectShape, ShapeRecord, TextShape } from "./model"; import type { EditorState } from "./reactivity"; import { getSelectedShapes, getShapesOnCurrentPage } from "./reactivity"; @@ -168,6 +168,9 @@ function shapeToSVG(shape: ShapeRecord, state: EditorState): string | null { case "text": { return textToSVG(shape, transform); } + case "markdown": { + return markdownToSVG(shape, transform); + } default: { return null; } @@ -247,6 +250,36 @@ function textToSVG(shape: TextShape, transform: string): string { }">${escapeXML(text)}`; } +/** + * Export markdown shape as SVG foreignObject + * + * Uses foreignObject to embed HTML for markdown rendering. + * + * For better compatibility, the markdown is exported as plain text with basic formatting preserved. + */ +function markdownToSVG(shape: MarkdownShape, transform: string): string { + const { md, w, h, fontSize, fontFamily, color, bg, border } = shape.props; + const width = w; + const height = h ?? fontSize * 10; + + const bgStyle = bg ? `background: ${escapeXML(bg)};` : "background: white;"; + const borderStyle = border ? `border: 1px solid ${escapeXML(border)};` : ""; + + const escapedMarkdown = escapeXML(md); + + return [ + ``, + `
`, + ` ${escapedMarkdown}`, + `
`, + `
`, + ].join("\n"); +} + /** * Escape special XML characters in strings. */ diff --git a/packages/core/src/geom.ts b/packages/core/src/geom.ts index 8d7298b..820f3f3 100644 --- a/packages/core/src/geom.ts +++ b/packages/core/src/geom.ts @@ -6,6 +6,7 @@ import type { BrushConfig, EllipseShape, LineShape, + MarkdownShape, RectShape, ShapeRecord, StrokePoint, @@ -44,6 +45,9 @@ export function shapeBounds(shape: ShapeRecord): Box2 { case "stroke": { return strokeBounds(shape); } + case "markdown": { + return markdownBounds(shape); + } } } @@ -139,6 +143,26 @@ function textBounds(shape: TextShape): Box2 { return Box2Ops.fromPoints(translatedCorners); } +/** + * Get bounds for a markdown block shape + */ +function markdownBounds(shape: MarkdownShape): Box2 { + const { w, h, fontSize } = shape.props; + const { x, y, rot } = shape; + + const width = w; + const height = h ?? fontSize * 10; + + if (rot === 0) { + return Box2Ops.create(x, y, x + width, y + height); + } + + const corners = [{ x: 0, y: 0 }, { x: width, y: 0 }, { x: width, y: height }, { x: 0, y: height }]; + const rotatedCorners = corners.map((corner) => rotatePoint(corner, rot)); + const translatedCorners = rotatedCorners.map((corner) => ({ x: corner.x + x, y: corner.y + y })); + return Box2Ops.fromPoints(translatedCorners); +} + /** * Compute outline polygon points for a stroke using perfect-freehand * @@ -323,6 +347,22 @@ export function pointInText(p: Vec2, shape: TextShape): boolean { return localP.x >= 0 && localP.x <= width && localP.y >= 0 && localP.y <= height; } +/** + * Check if a point is inside a markdown block shape + * + * @param p - Point in world coordinates + * @param shape - Markdown block shape + * @returns True if point is inside the markdown block bounds + */ +export function pointInMarkdown(p: Vec2, shape: MarkdownShape): boolean { + const { x, y, rot } = shape; + const { w, h, fontSize } = shape.props; + const localP = worldToLocal(p, x, y, rot); + const width = w; + const height = h ?? fontSize * 10; + return localP.x >= 0 && localP.x <= width && localP.y >= 0 && localP.y <= height; +} + /** * Check if a point is inside a polygon using ray casting algorithm * @@ -435,6 +475,12 @@ export function hitTestPoint(state: EditorState, worldPoint: Vec2, tolerance = 5 } break; } + case "markdown": { + if (pointInMarkdown(worldPoint, shape)) { + return shape.id; + } + break; + } case "stroke": { if (hitTestStroke(worldPoint, shape)) { return shape.id; diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index f7fe20f..101a87a 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -67,6 +67,24 @@ export type ArrowProps = { export type TextProps = { text: string; fontSize: number; fontFamily: string; color: string; w?: number }; +/** + * Markdown block properties + * - md: markdown source text + * - w: fixed width (required for layout) + * - h: auto-computed height from layout (optional override) + * - style: font and color settings + */ +export type MarkdownProps = { + md: string; + w: number; + h?: number; + fontSize: number; + fontFamily: string; + color: string; + bg?: string; + border?: string; +}; + /** * Point with optional pressure value (0-1) * Format: [x, y, pressure?] @@ -97,8 +115,7 @@ export type StrokeStyle = { color: string; opacity: number }; */ export type StrokeProps = { points: StrokePoint[]; style: StrokeStyle; brush: BrushConfig }; -export type ShapeType = "rect" | "ellipse" | "line" | "arrow" | "text" | "stroke"; - +export type ShapeType = "rect" | "ellipse" | "line" | "arrow" | "text" | "stroke" | "markdown"; export type BaseShape = { id: string; type: ShapeType; pageId: string; x: number; y: number; rot: number }; export type RectShape = BaseShape & { type: "rect"; props: RectProps }; export type EllipseShape = BaseShape & { type: "ellipse"; props: EllipseProps }; @@ -106,8 +123,9 @@ export type LineShape = BaseShape & { type: "line"; props: LineProps }; export type ArrowShape = BaseShape & { type: "arrow"; props: ArrowProps }; export type TextShape = BaseShape & { type: "text"; props: TextProps }; export type StrokeShape = BaseShape & { type: "stroke"; props: StrokeProps }; +export type MarkdownShape = BaseShape & { type: "markdown"; props: MarkdownProps }; -export type ShapeRecord = RectShape | EllipseShape | LineShape | ArrowShape | TextShape | StrokeShape; +export type ShapeRecord = RectShape | EllipseShape | LineShape | ArrowShape | TextShape | StrokeShape | MarkdownShape; export const ShapeRecord = { /** @@ -152,6 +170,13 @@ export const ShapeRecord = { return { id: id ?? createId("shape"), type: "stroke", pageId, x, y, rot: 0, props: properties }; }, + /** + * Create a markdown block shape + */ + createMarkdown(pageId: string, x: number, y: number, properties: MarkdownProps, id?: string): MarkdownShape { + return { id: id ?? createId("shape"), type: "markdown", pageId, x, y, rot: 0, props: properties }; + }, + /** * Clone a shape record */ @@ -180,6 +205,9 @@ export const ShapeRecord = { }, }; } + if (shape.type === "markdown") { + return { ...shape, props: { ...shape.props } }; + } return { ...shape, props: { ...shape.props } } as ShapeRecord; }, }; @@ -346,6 +374,19 @@ export function validateDoc(document: Document): ValidationResult { errors.push(`Stroke shape '${shapeId}' has invalid opacity`); } + break; + } + case "markdown": { + if (shape.props.fontSize <= 0) { + errors.push(`Markdown shape '${shapeId}' has invalid fontSize`); + } + if (shape.props.w <= 0) { + errors.push(`Markdown shape '${shapeId}' has invalid width`); + } + if (shape.props.h !== undefined && shape.props.h <= 0) { + errors.push(`Markdown shape '${shapeId}' has invalid height`); + } + break; } } diff --git a/packages/core/src/reactivity.ts b/packages/core/src/reactivity.ts index 4df02bc..2a85d1e 100644 --- a/packages/core/src/reactivity.ts +++ b/packages/core/src/reactivity.ts @@ -12,7 +12,7 @@ import { import type { Document, PageRecord, ShapeRecord } from "./model"; import { Document as DocumentOps } from "./model"; -export type ToolId = "select" | "rect" | "ellipse" | "line" | "arrow" | "text" | "pen"; +export type ToolId = "select" | "rect" | "ellipse" | "line" | "arrow" | "text" | "pen" | "markdown"; export type BindingPreview = { arrowId: string; targetShapeId: string; handle: "start" | "end" }; diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 6ca0143..cd05fc9 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -1,4 +1,5 @@ export * from "./base"; +export * from "./markdown"; export * from "./pen"; export * from "./select"; export * from "./shape"; diff --git a/packages/core/src/tools/markdown.ts b/packages/core/src/tools/markdown.ts new file mode 100644 index 0000000..2e5f179 --- /dev/null +++ b/packages/core/src/tools/markdown.ts @@ -0,0 +1,66 @@ +import type { Action } from "../actions"; +import { createId, ShapeRecord } from "../model"; +import type { EditorState, ToolId } from "../reactivity"; +import { getCurrentPage } from "../reactivity"; +import type { Tool } from "./base"; + +/** + * Markdown tool creates markdown block shapes on click + * + * Features: + * - Click to create a markdown block at the pointer position + * - Block is created with default content and dimensions + * - Shape is immediately selected after creation + */ +export class MarkdownTool implements Tool { + readonly id: ToolId = "markdown"; + + onEnter(state: EditorState): EditorState { + return state; + } + + onExit(state: EditorState): EditorState { + return state; + } + + onAction(state: EditorState, action: Action): EditorState { + switch (action.type) { + case "pointer-down": { + return this.handlePointerDown(state, action); + } + default: { + return state; + } + } + } + + private handlePointerDown(state: EditorState, action: Action): EditorState { + if (action.type !== "pointer-down") return state; + + const currentPage = getCurrentPage(state); + if (!currentPage) return state; + + const shapeId = createId("shape"); + + const shape = ShapeRecord.createMarkdown(currentPage.id, action.world.x, action.world.y, { + md: "# Markdown\n\nEdit me...", + w: 300, + h: 200, + fontSize: 16, + fontFamily: "sans-serif", + color: "#1f2933", + }, shapeId); + + const newPage = { ...currentPage, shapeIds: [...currentPage.shapeIds, shapeId] }; + + return { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [shapeId]: shape }, + pages: { ...state.doc.pages, [currentPage.id]: newPage }, + }, + ui: { ...state.ui, selectionIds: [shapeId] }, + }; + } +} diff --git a/packages/core/src/tools/select.ts b/packages/core/src/tools/select.ts index 56ef637..7a53562 100644 --- a/packages/core/src/tools/select.ts +++ b/packages/core/src/tools/select.ts @@ -576,7 +576,9 @@ export class SelectTool implements Tool { pointer: Vec2, handle: HandleKind, ): ShapeRecord | null { - if (initial.type !== "rect" && initial.type !== "ellipse" && initial.type !== "text") { + if ( + initial.type !== "rect" && initial.type !== "ellipse" && initial.type !== "text" && initial.type !== "markdown" + ) { return null; } let minX = bounds.min.x; @@ -633,6 +635,10 @@ export class SelectTool implements Tool { return { ...initial, x: minX, y: minY, props: { ...initial.props, w: width } }; } + if (initial.type === "markdown") { + return { ...initial, x: minX, y: minY, props: { ...initial.props, w: width, h: height } }; + } + // @ts-expect-error union mismatch return { ...initial, x: minX, y: minY, props: { ...initial.props, w: width, h: height } }; } @@ -756,7 +762,9 @@ export class SelectTool implements Tool { if (!this.toolState.rotationCenter || this.toolState.rotationStartAngle === null) { return null; } - if (initial.type !== "rect" && initial.type !== "ellipse" && initial.type !== "text") { + if ( + initial.type !== "rect" && initial.type !== "ellipse" && initial.type !== "text" && initial.type !== "markdown" + ) { return null; } const currentAngle = Math.atan2( diff --git a/packages/core/tests/markdown.test.ts b/packages/core/tests/markdown.test.ts new file mode 100644 index 0000000..9bf75ec --- /dev/null +++ b/packages/core/tests/markdown.test.ts @@ -0,0 +1,394 @@ +import { describe, expect, it } from "vitest"; +import { pointInMarkdown, shapeBounds } from "../src/geom"; +import type { MarkdownProps } from "../src/model"; +import { Document, PageRecord, ShapeRecord, validateDoc } from "../src/model"; +import { EditorState as EditorStateOps } from "../src/reactivity"; + +const createProps = (overrides?: Partial) => ({ + md: "# Hello World", + w: 300, + h: 200, + fontSize: 16, + fontFamily: "sans-serif", + color: "#1f2933", + ...overrides, +}); + +describe("MarkdownShape", () => { + const pageId = "page:test"; + + describe("createMarkdown", () => { + it("should create a markdown shape with generated ID", () => { + const props = createProps(); + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props); + + expect(shape.id).toMatch(/^shape:/); + expect(shape.type).toBe("markdown"); + expect(shape.pageId).toBe(pageId); + expect(shape.x).toBe(10); + expect(shape.y).toBe(20); + expect(shape.rot).toBe(0); + expect(shape.props).toEqual(props); + }); + + it("should create a markdown shape with custom ID", () => { + const props = createProps({ md: "# Test", color: "#000" }); + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props, "shape:custom"); + expect(shape.id).toBe("shape:custom"); + }); + + it("should create a markdown shape with optional bg and border", () => { + const props = createProps({ md: "# Styled", color: "#000", bg: "#ffffff", border: "#cccccc" }); + const shape = ShapeRecord.createMarkdown(pageId, 0, 0, props); + expect(shape.props.bg).toBe("#ffffff"); + expect(shape.props.border).toBe("#cccccc"); + }); + + it("should create a markdown shape without height (auto-computed)", () => { + const props = createProps({ md: "# Auto Height", w: 300, h: undefined, color: "#000" }); + const shape = ShapeRecord.createMarkdown(pageId, 0, 0, props); + expect(shape.props.h).toBeUndefined(); + }); + + it.each([{ md: "# Heading\n\nParagraph", w: 400, h: 300, fontSize: 18 }, { + md: "- List item 1\n- List item 2", + w: 200, + h: 150, + fontSize: 14, + }, { md: "```\ncode block\n```", w: 350, h: 250, fontSize: 12 }])( + "should create markdown with various content: %o", + ({ md, w, h, fontSize }) => { + const props: MarkdownProps = { md, w, h, fontSize, fontFamily: "sans-serif", color: "#000" }; + const shape = ShapeRecord.createMarkdown(pageId, 0, 0, props); + + expect(shape.props.md).toBe(md); + expect(shape.props.w).toBe(w); + expect(shape.props.h).toBe(h); + }, + ); + }); + + describe("clone", () => { + it("should clone a markdown shape", () => { + const props = createProps({ md: "# Clone Test", color: "#000" }); + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props); + const cloned = ShapeRecord.clone(shape); + + expect(cloned).toEqual(shape); + expect(cloned).not.toBe(shape); + expect(cloned.props).not.toBe(shape.props); + }); + + it("should deep clone props", () => { + const props = createProps({ md: "# Original", fontFamily: "sans-serif", color: "#000" }); + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props); + const cloned = ShapeRecord.clone(shape); + + if (cloned.type === "markdown") { + cloned.props.md = "# Modified"; + cloned.props.w = 400; + } + + expect(shape.props.md).toBe("# Original"); + expect(shape.props.w).toBe(300); + }); + }); + + describe("geometry", () => { + describe("shapeBounds", () => { + it("should compute bounds for markdown shape without rotation", () => { + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + const bounds = shapeBounds(shape); + expect(bounds.min.x).toBe(10); + expect(bounds.min.y).toBe(20); + expect(bounds.max.x).toBe(310); + expect(bounds.max.y).toBe(220); + }); + + it("should compute bounds for markdown shape with auto height", () => { + const shape = ShapeRecord.createMarkdown(pageId, 0, 0, createProps({ md: "# Test", color: "#000" })); + const bounds = shapeBounds(shape); + expect(bounds.min.x).toBe(0); + expect(bounds.min.y).toBe(0); + expect(bounds.max.x).toBe(300); + expect(bounds.max.y).toBe(160); + }); + + it("should compute rotated bounds correctly", () => { + const shape = ShapeRecord.createMarkdown( + pageId, + 100, + 100, + createProps({ md: "# Test", w: 200, h: 100, color: "#000" }), + ); + shape.rot = Math.PI / 4; + + const bounds = shapeBounds(shape); + + expect(bounds.min.x).toBeLessThan(101); + expect(bounds.min.y).toBeLessThan(101); + expect(bounds.max.x).toBeGreaterThan(200); + expect(bounds.max.y).toBeGreaterThan(150); + }); + }); + + describe("pointInMarkdown", () => { + it("should return true for point inside markdown block", () => { + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + expect(pointInMarkdown({ x: 100, y: 100 }, shape)).toBe(true); + }); + + it("should return false for point outside markdown block", () => { + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + expect(pointInMarkdown({ x: 400, y: 100 }, shape)).toBe(false); + expect(pointInMarkdown({ x: 100, y: 300 }, shape)).toBe(false); + }); + + it("should handle edge cases on bounds", () => { + const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + expect(pointInMarkdown({ x: 10, y: 20 }, shape)).toBe(true); + expect(pointInMarkdown({ x: 310, y: 220 }, shape)).toBe(true); + expect(pointInMarkdown({ x: 9, y: 20 }, shape)).toBe(false); + expect(pointInMarkdown({ x: 311, y: 220 }, shape)).toBe(false); + }); + }); + }); + + describe("validation", () => { + it("should validate markdown shape with all required fields", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + const shape = ShapeRecord.createMarkdown("page1", 0, 0, createProps({ md: "# Valid", color: "#000" }), "shape1"); + + page.shapeIds = ["shape1"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: shape }; + + const result = validateDoc(doc); + expect(result.ok).toBe(true); + }); + + it("should reject markdown with invalid fontSize", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + const shape = ShapeRecord.createMarkdown( + "page1", + 0, + 0, + createProps({ md: "# Test", fontSize: 0, color: "#000" }), + "shape1", + ); + + page.shapeIds = ["shape1"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: shape }; + + const result = validateDoc(doc); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toContain("Markdown shape 'shape1' has invalid fontSize"); + } + }); + + it("should reject markdown with invalid width", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + const shape = ShapeRecord.createMarkdown( + "page1", + 0, + 0, + createProps({ md: "# Test", w: 0, h: 200, color: "#000" }), + "shape1", + ); + + page.shapeIds = ["shape1"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: shape }; + + const result = validateDoc(doc); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toContain("Markdown shape 'shape1' has invalid width"); + } + }); + + it("should reject markdown with negative height", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + const shape = ShapeRecord.createMarkdown( + "page1", + 0, + 0, + createProps({ md: "# Test", h: -100, color: "#000" }), + "shape1", + ); + + page.shapeIds = ["shape1"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: shape }; + + const result = validateDoc(doc); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errors).toContain("Markdown shape 'shape1' has invalid height"); + } + }); + + it("should accept markdown with undefined height (auto-computed)", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + const shape = ShapeRecord.createMarkdown( + "page1", + 0, + 0, + createProps({ md: "# Test", h: undefined, color: "#000" }), + "shape1", + ); + + page.shapeIds = ["shape1"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: shape }; + + const result = validateDoc(doc); + + expect(result.ok).toBe(true); + }); + }); + + describe("JSON serialization", () => { + it("should round-trip markdown shape", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + const shape = ShapeRecord.createMarkdown( + "page1", + 10, + 20, + createProps({ + md: "# Hello World\n\nThis is a **markdown** block.", + color: "#1f2933", + bg: "#ffffff", + border: "#e0e0e0", + }), + "shape1", + ); + + page.shapeIds = ["shape1"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: shape }; + + const json = JSON.stringify(doc); + const parsed = JSON.parse(json); + + expect(parsed).toEqual(doc); + expect(validateDoc(parsed).ok).toBe(true); + }); + + it("should round-trip markdown shape with complex markdown content", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + const markdown = `# Markdown Test + +## Features + +- **Bold text** +- *Italic text* +- \`code\` + +### Code Block + +\`\`\`javascript +const hello = "world"; +\`\`\` + +1. Ordered +2. List +3. Items`; + + const shape = ShapeRecord.createMarkdown("page1", 0, 0, { + md: markdown, + w: 400, + h: 500, + fontSize: 14, + fontFamily: "system-ui", + color: "#000000", + }, "shape1"); + + page.shapeIds = ["shape1"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: shape }; + + const json = JSON.stringify(doc); + const parsed = JSON.parse(json); + + expect(parsed).toEqual(doc); + expect(validateDoc(parsed).ok).toBe(true); + }); + + it("should round-trip document with markdown and other shapes", () => { + const doc = Document.create(); + const page = PageRecord.create("Page 1", "page1"); + + const rect = ShapeRecord.createRect( + "page1", + 0, + 0, + { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 5 }, + "shape1", + ); + + const markdown = ShapeRecord.createMarkdown("page1", 150, 100, { + md: "# Markdown\n\nNext to a rectangle", + w: 300, + h: 200, + fontSize: 16, + fontFamily: "sans-serif", + color: "#000", + }, "shape2"); + + const text = ShapeRecord.createText("page1", 500, 200, { + text: "Plain text", + fontSize: 18, + fontFamily: "Arial", + color: "#333", + }, "shape3"); + + page.shapeIds = ["shape1", "shape2", "shape3"]; + doc.pages = { page1: page }; + doc.shapes = { shape1: rect, shape2: markdown, shape3: text }; + + const json = JSON.stringify(doc); + const parsed = JSON.parse(json); + + expect(parsed).toEqual(doc); + expect(validateDoc(parsed).ok).toBe(true); + }); + }); + + describe("integration with EditorState", () => { + it("should work in EditorState with markdown shapes", () => { + const state = EditorStateOps.create(); + const page = PageRecord.create("Test Page", "page1"); + const markdown = ShapeRecord.createMarkdown( + "page1", + 0, + 0, + createProps({ md: "# Test", color: "#000" }), + "shape1", + ); + + page.shapeIds = ["shape1"]; + state.doc.pages = { page1: page }; + state.doc.shapes = { shape1: markdown }; + state.ui.currentPageId = "page1"; + + const cloned = EditorStateOps.clone(state); + + expect(cloned).toEqual(state); + expect(cloned).not.toBe(state); + expect(cloned.doc.shapes.shape1).not.toBe(state.doc.shapes.shape1); + }); + }); +}); diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 36106a5..39d14ff 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -33,5 +33,12 @@ "typescript-eslint": "^8.50.1", "vitest": "^4.0.16" }, - "dependencies": { "inkfinite-core": "workspace:*" } + "dependencies": { + "inkfinite-core": "workspace:*", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "unified": "^11.0.5" + } } diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index 6a3a002..ff0e58d 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -5,6 +5,7 @@ import type { EditorState, EllipseShape, LineShape, + MarkdownShape, RectShape, ShapeRecord, Store, @@ -370,6 +371,10 @@ function drawShape(context: CanvasRenderingContext2D, state: EditorState, shape: drawText(context, shape); break; } + case "markdown": { + drawMarkdown(context, shape); + break; + } case "stroke": { drawStroke(context, shape); break; @@ -603,6 +608,215 @@ function drawText(context: CanvasRenderingContext2D, shape: TextShape) { } } +/** + * Parse and render markdown to canvas + * + * Renders markdown with basic formatting: + * - Headings (h1-h6) with appropriate sizes + * - Bold (**text** or __text__) + * - Italic (*text* or _text_) + * - Code (`code`) + * - Paragraphs with line wrapping + * - Lists (ordered and unordered) + * - Code blocks (```) + */ +function drawMarkdown(context: CanvasRenderingContext2D, shape: MarkdownShape) { + const { md, w, h, fontSize, fontFamily, color, bg, border } = shape.props; + + const width = w; + const height = h ?? fontSize * 10; + + context.fillStyle = bg ?? "#ffffff"; + context.fillRect(0, 0, width, height); + + if (border) { + context.strokeStyle = border; + context.lineWidth = 1; + context.strokeRect(0, 0, width, height); + } + + context.fillStyle = color; + context.textBaseline = "top"; + + const padding = 8; + let yOffset = padding; + const lineHeight = fontSize * 1.4; + + const lines = md.split("\n"); + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + let line = lines[lineIndex]; + + if (yOffset + lineHeight > height - padding) break; + + let currentFontSize = fontSize; + let currentStyle = "normal"; + let currentWeight = "normal"; + let prefix = ""; + + if (line.startsWith("```")) { + context.fillStyle = "#f4f4f4"; + const codeBlockLines = []; + lineIndex++; + while (lineIndex < lines.length && !lines[lineIndex].startsWith("```")) { + codeBlockLines.push(lines[lineIndex]); + lineIndex++; + } + + const codeBlockHeight = codeBlockLines.length * lineHeight + padding * 2; + if (yOffset + codeBlockHeight <= height - padding) { + context.fillRect(padding, yOffset, width - padding * 2, codeBlockHeight); + + context.fillStyle = "#333"; + context.font = `normal normal ${fontSize}px monospace`; + + for (const [index, codeLine] of codeBlockLines.entries()) { + context.fillText(codeLine, padding + 4, yOffset + padding + index * lineHeight); + } + + yOffset += codeBlockHeight + padding; + } + + context.fillStyle = color; + context.font = `${currentWeight} ${currentStyle} ${currentFontSize}px ${fontFamily}`; + continue; + } + + if (line.match(/^#{1,6}\s/)) { + const match = line.match(/^(#{1,6})\s(.*)$/); + if (match) { + const level = match[1].length; + line = match[2]; + currentFontSize = fontSize * (2 - level * 0.15); + currentWeight = "bold"; + } + } else if (line.match(/^[-*+]\s/)) { + prefix = "• "; + line = line.replace(/^[-*+]\s/, ""); + } else if (line.match(/^\d+\.\s/)) { + const match = line.match(/^(\d+)\.\s(.*)$/); + if (match) { + prefix = `${match[1]}. `; + line = match[2]; + } + } + + line = prefix + line; + + line = line.replace(/`([^`]+)`/g, "$1"); + + context.font = `${currentWeight} ${currentStyle} ${currentFontSize}px ${fontFamily}`; + + const wrappedLines = wrapText(context, line, width - padding * 2); + + for (const wrappedLine of wrappedLines) { + if (yOffset + currentFontSize * 1.4 > height - padding) break; + + const styledLine = wrappedLine; + let xOffset = padding; + + const segments = parseInlineStyles(styledLine); + + for (const segment of segments) { + const { text: segmentText, bold, italic, code } = segment; + + if (code) { + context.fillStyle = "#f4f4f4"; + const metrics = context.measureText(segmentText); + context.fillRect(xOffset, yOffset, metrics.width + 4, currentFontSize * 1.2); + context.fillStyle = "#333"; + context.font = `normal normal ${currentFontSize * 0.9}px monospace`; + context.fillText(segmentText, xOffset + 2, yOffset); + xOffset += metrics.width + 4; + context.fillStyle = color; + context.font = `${currentWeight} ${currentStyle} ${currentFontSize}px ${fontFamily}`; + } else { + const weight = bold ? "bold" : currentWeight; + const style = italic ? "italic" : currentStyle; + context.font = `${weight} ${style} ${currentFontSize}px ${fontFamily}`; + context.fillText(segmentText, xOffset, yOffset); + const metrics = context.measureText(segmentText); + xOffset += metrics.width; + context.font = `${currentWeight} ${currentStyle} ${currentFontSize}px ${fontFamily}`; + } + } + + yOffset += currentFontSize * 1.4; + } + } +} + +/** + * Parse inline markdown styles (bold, italic, code) into segments + */ +function parseInlineStyles(text: string): Array<{ text: string; bold: boolean; italic: boolean; code: boolean }> { + const segments: Array<{ text: string; bold: boolean; italic: boolean; code: boolean }> = []; + + const codeRegex = /`([^`]+)`/g; + const parts = []; + let lastIndex = 0; + let match; + + while ((match = codeRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push({ text: text.slice(lastIndex, match.index), code: false }); + } + parts.push({ text: match[1], code: true }); + lastIndex = codeRegex.lastIndex; + } + + if (lastIndex < text.length) { + parts.push({ text: text.slice(lastIndex), code: false }); + } + + for (const part of parts) { + if (part.code) { + segments.push({ text: part.text, bold: false, italic: false, code: true }); + } else { + const boldItalicRegex = /(\*\*\*|___)([^*_]+)(\*\*\*|___)|(\*\*|__)([^*_]+)(\*\*|__)|(\*|_)([^*_]+)(\*|_)/g; + let lastPartIndex = 0; + let partMatch; + + while ((partMatch = boldItalicRegex.exec(part.text)) !== null) { + if (partMatch.index > lastPartIndex) { + segments.push({ + text: part.text.slice(lastPartIndex, partMatch.index), + bold: false, + italic: false, + code: false, + }); + } + + if (partMatch[1]) { + segments.push({ text: partMatch[2], bold: true, italic: true, code: false }); + } else if (partMatch[4]) { + segments.push({ text: partMatch[5], bold: true, italic: false, code: false }); + } else if (partMatch[7]) { + segments.push({ text: partMatch[8], bold: false, italic: true, code: false }); + } + + lastPartIndex = boldItalicRegex.lastIndex; + } + + if (lastPartIndex < part.text.length) { + segments.push({ text: part.text.slice(lastPartIndex), bold: false, italic: false, code: false }); + } + + if (segments.length === 0 || lastPartIndex === 0) { + if (segments.length === 0) { + segments.push({ text: part.text, bold: false, italic: false, code: false }); + } + } + } + } + + if (segments.length === 0) { + segments.push({ text, bold: false, italic: false, code: false }); + } + + return segments; +} + /** * Draw a stroke shape (freehand drawing) */ @@ -732,6 +946,13 @@ function drawSelection( context.strokeRect(0, 0, width, height); break; } + case "markdown": { + const { w, h, fontSize } = shape.props; + const width = w; + const height = h ?? fontSize * 10; + context.strokeRect(0, 0, width, height); + break; + } case "stroke": { const { points, brush } = shape.props; if (points.length >= 2) { @@ -824,7 +1045,7 @@ function drawHandles( function getHandlesForShape(state: EditorState, shape: ShapeRecord): HandleVisual[] { const handles: HandleVisual[] = []; - if (shape.type === "rect" || shape.type === "ellipse" || shape.type === "text") { + if (shape.type === "rect" || shape.type === "ellipse" || shape.type === "text" || shape.type === "markdown") { const bounds = shapeBounds(shape); const minX = bounds.min.x; const maxX = bounds.max.x; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b84926b..45078e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,21 @@ importers: inkfinite-core: specifier: workspace:* version: link:../core + rehype-stringify: + specifier: ^10.0.1 + version: 10.0.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 + remark-rehype: + specifier: ^11.1.2 + version: 11.1.2 + unified: + specifier: ^11.0.5 + version: 11.0.5 devDependencies: '@eslint/js': specifier: ^9.39.2 @@ -896,18 +911,30 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/jsdom@27.0.0': resolution: {integrity: sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.10.4': resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} @@ -917,6 +944,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1035,6 +1065,9 @@ packages: resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitest/browser-playwright@4.0.16': resolution: {integrity: sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==} peerDependencies: @@ -1134,6 +1167,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1170,6 +1206,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.1: resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} @@ -1178,6 +1217,15 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1200,6 +1248,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1247,6 +1298,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1257,12 +1311,19 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} devalue@5.6.1: resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dexie@4.2.1: resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==} @@ -1307,6 +1368,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true @@ -1381,6 +1446,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fake-indexeddb@6.2.5: resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} engines: {node: '>=18'} @@ -1451,6 +1519,12 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -1461,6 +1535,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1501,6 +1578,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1591,6 +1672,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} @@ -1605,9 +1689,132 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1765,6 +1972,9 @@ packages: engines: {node: '>=14'} hasBin: true + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1783,6 +1993,21 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1864,12 +2089,18 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1940,6 +2171,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2003,6 +2240,24 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unrun@0.2.20: resolution: {integrity: sha512-YhobStTk93HYRN/4iBs3q3/sd7knvju1XrzwwrVVfRujyTG1K88hGONIxCoJN0PWBuO+BX7fFiHH0sVDfE3MWw==} engines: {node: '>=20.19.0'} @@ -2027,6 +2282,12 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-devtools-json@1.0.0: resolution: {integrity: sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw==} peerDependencies: @@ -2189,6 +2450,9 @@ packages: zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@acemir/cssom@0.9.29': {} @@ -2706,10 +2970,18 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/jsdom@27.0.0': dependencies: '@types/node': 25.0.3 @@ -2718,6 +2990,12 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@24.10.4': dependencies: undici-types: 7.16.0 @@ -2728,6 +3006,8 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2910,6 +3190,8 @@ snapshots: '@typescript-eslint/types': 8.50.1 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@vitest/browser-playwright@4.0.16(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(yaml@2.8.2))(vitest@4.0.16)': dependencies: '@vitest/browser': 4.0.16(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(yaml@2.8.2))(vitest@4.0.16) @@ -3080,6 +3362,8 @@ snapshots: axobject-query@4.1.0: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} bidi-js@1.0.3: @@ -3134,6 +3418,8 @@ snapshots: callsites@3.1.0: {} + ccount@2.0.1: {} + chai@6.2.1: {} chalk@4.1.2: @@ -3141,6 +3427,12 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -3161,6 +3453,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + concat-map@0.0.1: {} confbox@0.2.2: {} @@ -3199,16 +3493,26 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + deep-is@0.1.4: {} deepmerge@4.3.1: {} defu@6.1.4: {} + dequal@2.0.3: {} + destr@2.0.5: {} devalue@5.6.1: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dexie@4.2.1: {} dotenv@17.2.3: {} @@ -3266,6 +3570,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): dependencies: eslint: 9.39.2(jiti@2.6.1) @@ -3370,6 +3676,8 @@ snapshots: exsolve@1.0.8: {} + extend@3.0.2: {} + fake-indexeddb@6.2.5: {} fast-deep-equal@3.1.3: {} @@ -3427,6 +3735,24 @@ snapshots: has-flag@4.0.0: {} + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hookable@5.5.3: {} html-encoding-sniffer@4.0.0: @@ -3435,6 +3761,8 @@ snapshots: html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -3472,6 +3800,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-reference@3.0.3: @@ -3569,6 +3899,8 @@ snapshots: lodash.merge@4.6.2: {} + longest-streak@3.1.0: {} + lru-cache@11.2.4: {} magic-string@0.30.21: @@ -3585,8 +3917,315 @@ snapshots: dependencies: semver: 7.7.3 + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.12.2: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -3719,6 +4358,8 @@ snapshots: prettier@3.7.4: {} + property-information@7.1.0: {} + punycode@2.3.1: {} quansync@1.0.0: {} @@ -3732,6 +4373,46 @@ snapshots: readdirp@5.0.0: {} + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -3835,10 +4516,17 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + stackback@0.0.2: {} std-env@3.10.0: {} + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-json-comments@3.1.1: {} supports-color@7.2.0: @@ -3917,6 +4605,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -3985,6 +4677,39 @@ snapshots: undici-types@7.16.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unrun@0.2.20: dependencies: rolldown: 1.0.0-beta.55 @@ -3999,6 +4724,16 @@ snapshots: uuid@13.0.0: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-plugin-devtools-json@1.0.0(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(yaml@2.8.2)): dependencies: uuid: 11.1.0 @@ -4160,3 +4895,5 @@ snapshots: yocto-queue@0.1.0: {} zimmerframe@1.1.4: {} + + zwitch@2.0.4: {} -- 2.51.2