@lapismd/mira 0.0.1 → 0.0.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # @lapismd/mira
2
2
 
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#3](https://github.com/lapismd/mira-mde/pull/3) [`31e2502`](https://github.com/lapismd/mira-mde/commit/31e2502e185f883390165851e146a3823c6b9274) Thanks [@stevejuma](https://github.com/stevejuma)! - Relicense the public Mira packages to Apache 2.0.
8
+
9
+ - [#7](https://github.com/lapismd/mira-mde/pull/7) [`215a690`](https://github.com/lapismd/mira-mde/commit/215a6906130dbcf06f0a649e8a76074c6e24612f) Thanks [@stevejuma](https://github.com/stevejuma)! - Render list bullets, hanging indentation, and indentation guides when a consumer
10
+ mounts a Markdown editor directly on CodeMirror's root. Wrapped editors retain
11
+ the same marker geometry and caret behavior.
12
+
13
+ - [#7](https://github.com/lapismd/mira-mde/pull/7) [`215a690`](https://github.com/lapismd/mira-mde/commit/215a6906130dbcf06f0a649e8a76074c6e24612f) Thanks [@stevejuma](https://github.com/stevejuma)! - Preserve inline checkbox labels in live-preview editors embedded inside rendered
14
+ Markdown by excluding CodeMirror cursor-buffer images from Markdown image styles.
15
+
16
+ Refresh the repository's renderer and release-tool dependencies to patched
17
+ versions, retaining math rendering and trust restrictions in regression coverage.
18
+
19
+ - [#7](https://github.com/lapismd/mira-mde/pull/7) [`215a690`](https://github.com/lapismd/mira-mde/commit/215a6906130dbcf06f0a649e8a76074c6e24612f) Thanks [@stevejuma](https://github.com/stevejuma)! - Declare the reviewed KaTeX stylesheet package as a runtime dependency so public
20
+ Mira, editor, and React CSS resolves in isolated package-manager installations.
21
+
22
+ ## 0.0.2
23
+
24
+ ### Patch Changes
25
+
26
+ - [`dd83f89`](https://github.com/lapismd/mira-mde/commit/dd83f89e474e9ae0e4fdd8c7cdfc9825e62b4a0d) Thanks [@stevejuma](https://github.com/stevejuma)! - Refresh unfocused frontmatter text controls after an external controller update, including after prior user editing, and preserve Live Preview disclosure across CodeMirror widget remounts.
27
+
28
+ - [`9e5d84f`](https://github.com/lapismd/mira-mde/commit/9e5d84f8148cd2d0a6008b31f04d943e1c0d9477) Thanks [@stevejuma](https://github.com/stevejuma)! - Keep ordinary parentheses visible in live preview, wrap editable table cells without overflow, and let source table rows scroll without scrollbar chrome.
29
+
30
+ - [`99fdddc`](https://github.com/lapismd/mira-mde/commit/99fdddcbe134eec42abfdfe4de951cc4a48f3aab) Thanks [@stevejuma](https://github.com/stevejuma)! - Keep Find and Replace input and placeholder typography compact across consumer base styles with a public size token.
31
+
32
+ - [`a9ea809`](https://github.com/lapismd/mira-mde/commit/a9ea8098a843452d7c4e122ab7ed190c48c84dcd) Thanks [@stevejuma](https://github.com/stevejuma)! - Present Find and Replace with pill-shaped fields, a search glyph, inline options, borderless actions, and no panel divider.
33
+
34
+ - [`990662d`](https://github.com/lapismd/mira-mde/commit/990662d2f7fc9fbdfa1cff6ee9c643aa2f9478b0) Thanks [@stevejuma](https://github.com/stevejuma)! - Preserve an internal-link trigger's inherited Mira theme and color mode when its hover preview portals to the owner document.
35
+
36
+ - [`bfc57f4`](https://github.com/lapismd/mira-mde/commit/bfc57f4fe8084e20644a34a7bb6c5b96719fb442) Thanks [@stevejuma](https://github.com/stevejuma)! - Avoid repeating an image embed's visible figure caption through its nested image alternative text.
37
+
38
+ - [`937ecd0`](https://github.com/lapismd/mira-mde/commit/937ecd04fe49b5c83b64df7d3dcd31257e2e57ff) Thanks [@stevejuma](https://github.com/stevejuma)! - Keep live-preview continuation indentation stable while CodeMirror rebuilds
39
+ line decorations after focus and selection changes.
40
+
41
+ - [`5165834`](https://github.com/lapismd/mira-mde/commit/5165834d3dd32a21f9d9703cd282511341c1ecf0) Thanks [@stevejuma](https://github.com/stevejuma)! - Keep contextual block-toolbar portals out of document layout and remove them
42
+ with their owning CodeMirror view.
43
+
44
+ - [`f83553a`](https://github.com/lapismd/mira-mde/commit/f83553a5fbf6f3fc9dfb6b1057ddac82b6a1b2c8) Thanks [@stevejuma](https://github.com/stevejuma)! - Align editable Markdown hover cards around a minimal padded presentation and persistence-safe outside-click dismissal.
45
+
46
+ - [`74963d2`](https://github.com/lapismd/mira-mde/commit/74963d237be51561a6d5282ec6f48e90736a1dd1) Thanks [@stevejuma](https://github.com/stevejuma)! - Preserve the active pipe-table column when Enter moves to the next row.
47
+
48
+ - [`7d2dd4b`](https://github.com/lapismd/mira-mde/commit/7d2dd4bdb2f9d78a760589f1e11450e4de5eb501) Thanks [@stevejuma](https://github.com/stevejuma)! - Keep the Find and Replace panel wrapper transparent so it does not paint a separate surface behind the editor controls.
49
+
50
+ - [`f573127`](https://github.com/lapismd/mira-mde/commit/f573127bb1d89c7da313eb897ba23ceef3b2236a) Thanks [@stevejuma](https://github.com/stevejuma)! - Allow consumers to compose Mira's complete portable authoring stack without its duplicate base CodeMirror setup.
51
+
3
52
  ## 0.0.1
4
53
 
5
54
  ### Patch Changes
package/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- AGPL-3.0-or-later
1
+ Apache-2.0
2
2
 
3
- This project is licensed under the GNU Affero General Public License v3.0 or later.
4
- See <https://www.gnu.org/licenses/agpl-3.0.en.html>.
3
+ This project is licensed under the Apache License 2.0.
4
+ See <https://www.apache.org/licenses/LICENSE-2.0>.
@@ -11,6 +11,69 @@ describe("createRichEditorExtensions", () => {
11
11
  it("can be disabled", () => {
12
12
  expect(createRichEditorExtensions({ enabled: false })).toEqual([]);
13
13
  });
14
+ it("preserves a local frontmatter disclosure across preview widget remounts", async () => {
15
+ const parent = document.createElement("div");
16
+ document.body.append(parent);
17
+ const source = "---\ntitle: Portable\n---\n\n# Note";
18
+ const view = new EditorView({
19
+ doc: source,
20
+ selection: { anchor: source.length },
21
+ extensions: [
22
+ createMarkdownCodeMirrorExtensions(),
23
+ createRichEditorExtensions({
24
+ livePreview: true,
25
+ frontmatterOpen: false,
26
+ }),
27
+ ],
28
+ parent,
29
+ });
30
+ await nextFrame();
31
+ await nextFrame();
32
+ const expand = parent.querySelector('.mira-rich-widget--frontmatter button[aria-label="Expand properties"]');
33
+ expect(expand).not.toBeNull();
34
+ expand?.click();
35
+ await nextFrame();
36
+ expect(parent.querySelector(".md-frontmatter__content")).not.toBeNull();
37
+ view.dispatch({ selection: { anchor: 2 } });
38
+ await nextFrame();
39
+ expect(parent.querySelector(".mira-rich-widget--frontmatter")).toBeNull();
40
+ view.dispatch({ selection: { anchor: source.length } });
41
+ await nextFrame();
42
+ await nextFrame();
43
+ expect(parent.querySelector('.mira-rich-widget--frontmatter button[aria-label="Collapse properties"]')).not.toBeNull();
44
+ expect(parent.querySelector(".md-frontmatter__content")).not.toBeNull();
45
+ view.destroy();
46
+ parent.remove();
47
+ });
48
+ it("keeps an interactive frontmatter menu mounted while its trigger receives focus", async () => {
49
+ const parent = document.createElement("div");
50
+ document.body.append(parent);
51
+ const source = "---\ntitle: Portable\n---\n\n# Note";
52
+ const view = new EditorView({
53
+ doc: source,
54
+ selection: { anchor: source.length },
55
+ extensions: [
56
+ createMarkdownCodeMirrorExtensions(),
57
+ createRichEditorExtensions({
58
+ livePreview: true,
59
+ frontmatterOpen: true,
60
+ }),
61
+ ],
62
+ parent,
63
+ });
64
+ await nextFrame();
65
+ await nextFrame();
66
+ const trigger = parent.querySelector('button[aria-label="Property options for title"]');
67
+ expect(trigger).not.toBeNull();
68
+ trigger?.focus();
69
+ trigger?.click();
70
+ await nextFrame();
71
+ expect(trigger?.isConnected).toBe(true);
72
+ expect(trigger?.getAttribute("aria-expanded")).toBe("true");
73
+ expect(document.body.querySelector('[role="menu"][aria-label="Property options for title"]')).not.toBeNull();
74
+ view.destroy();
75
+ parent.remove();
76
+ });
14
77
  it("renders and reveals a seeded divider comment and rule as one widget", async () => {
15
78
  const parent = document.createElement("div");
16
79
  document.body.append(parent);
@@ -1 +1 @@
1
- {"version":3,"file":"preview-widgets.d.ts","sourceRoot":"","sources":["../../../../../src/internal/codemirror/rich/widgets/preview-widgets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAc1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAUtD,qBAAa,kBAAmB,SAAQ,UAAU;IAE9C,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;QACtC,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,qBAAqB,CAAC;KAChC;IAKM,EAAE,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO;IAgB/C,IAAa,eAAe,IAAI,MAAM,CAErC;IAEQ,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW;IA6H7C,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,2BAA2B;IAiBnC,OAAO,CAAC,0BAA0B;IAwDzB,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAQ/B,WAAW,IAAI,OAAO;CAGhC;AAED,qBAAa,oBAAqB,SAAQ,UAAU;IAEhD,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,qBAAqB,CAAC;KAChC;IAKM,EAAE,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO;IAQxC,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW;IA+CpC,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAQ/B,WAAW,IAAI,OAAO;CAGhC;AAED,qBAAa,gBAAiB,SAAQ,UAAU;IAE5C,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,qBAAqB,CAAC;KAChC;IAKM,EAAE,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO;IAQpC,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW;IAwCpC,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAQ/B,WAAW,IAAI,OAAO;CAGhC"}
1
+ {"version":3,"file":"preview-widgets.d.ts","sourceRoot":"","sources":["../../../../../src/internal/codemirror/rich/widgets/preview-widgets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAc1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAetD,qBAAa,kBAAmB,SAAQ,UAAU;IAE9C,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;QACtC,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,qBAAqB,CAAC;KAChC;IAKM,EAAE,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO;IAgB/C,IAAa,eAAe,IAAI,MAAM,CAErC;IAEQ,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW;IAoK7C,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,2BAA2B;IAiBnC,OAAO,CAAC,0BAA0B;IAwDzB,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAU/B,WAAW,IAAI,OAAO;CAGhC;AAED,qBAAa,oBAAqB,SAAQ,UAAU;IAEhD,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,qBAAqB,CAAC;KAChC;IAKM,EAAE,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO;IAQxC,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW;IA+CpC,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAQ/B,WAAW,IAAI,OAAO;CAGhC;AAED,qBAAa,gBAAiB,SAAQ,UAAU;IAE5C,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,qBAAqB,CAAC;KAChC;IAKM,EAAE,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO;IAQpC,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW;IAwCpC,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAQ/B,WAAW,IAAI,OAAO;CAGhC"}
@@ -8,6 +8,8 @@ import { shouldActivateEditablePreview, PREVIEW_INTERACTIVE_SELECTOR, } from "..
8
8
  import { estimateMarkdownBlockHeight } from "../utils/height-estimates";
9
9
  import { createSourceToggleButton, selectWidgetSource } from "./source-toggle";
10
10
  const previewWidgetMounts = new WeakMap();
11
+ const previewWidgetObservers = new WeakMap();
12
+ const frontmatterDisclosureByView = new WeakMap();
11
13
  export class BlockPreviewWidget extends WidgetType {
12
14
  config;
13
15
  constructor(config) {
@@ -65,6 +67,20 @@ export class BlockPreviewWidget extends WidgetType {
65
67
  selectWidgetSource(view, this.config.from, this.config.to);
66
68
  }));
67
69
  let pendingFrontmatterValue = "";
70
+ const configuredFrontmatterOpen = this.config.options.frontmatterOpen ?? true;
71
+ const retainedFrontmatterDisclosure = this.config.nodeName === "Frontmatter"
72
+ ? frontmatterDisclosureByView.get(view)
73
+ : undefined;
74
+ const frontmatterOpen = retainedFrontmatterDisclosure?.configuredOpen ===
75
+ configuredFrontmatterOpen
76
+ ? retainedFrontmatterDisclosure.open
77
+ : configuredFrontmatterOpen;
78
+ if (this.config.nodeName === "Frontmatter") {
79
+ frontmatterDisclosureByView.set(view, {
80
+ configuredOpen: configuredFrontmatterOpen,
81
+ open: frontmatterOpen,
82
+ });
83
+ }
68
84
  const component = mount(MarkdownPreview, {
69
85
  target: container,
70
86
  props: {
@@ -75,7 +91,7 @@ export class BlockPreviewWidget extends WidgetType {
75
91
  linkResolver: this.config.options.linkResolver,
76
92
  assetResolver: this.config.options.assetResolver,
77
93
  fileAdapter: this.config.options.fileAdapter,
78
- frontmatterOpen: this.config.options.frontmatterOpen ?? true,
94
+ frontmatterOpen,
79
95
  frontmatterConfig: this.config.options.frontmatterConfig,
80
96
  onChange: (replacement, from, to) => {
81
97
  const absoluteFrom = this.absoluteMarkdownOffset(from);
@@ -94,6 +110,28 @@ export class BlockPreviewWidget extends WidgetType {
94
110
  },
95
111
  });
96
112
  previewWidgetMounts.set(container, component);
113
+ if (this.config.nodeName === "Frontmatter") {
114
+ const retainDisclosure = () => {
115
+ const expanded = container
116
+ .querySelector(".md-frontmatter__trigger")
117
+ ?.getAttribute("aria-expanded");
118
+ if (expanded !== "true" && expanded !== "false")
119
+ return;
120
+ frontmatterDisclosureByView.set(view, {
121
+ configuredOpen: configuredFrontmatterOpen,
122
+ open: expanded === "true",
123
+ });
124
+ };
125
+ retainDisclosure();
126
+ const observer = new MutationObserver(retainDisclosure);
127
+ observer.observe(container, {
128
+ attributes: true,
129
+ attributeFilter: ["aria-expanded"],
130
+ childList: true,
131
+ subtree: true,
132
+ });
133
+ previewWidgetObservers.set(container, observer);
134
+ }
97
135
  if (this.config.nodeName === "HorizontalRule") {
98
136
  container.addEventListener(MIRA_DOODLE_DIVIDER_CONTROL_EVENT, (event) => {
99
137
  this.handleDoodleDividerControl(view, event);
@@ -182,6 +220,8 @@ export class BlockPreviewWidget extends WidgetType {
182
220
  view.scrollDOM.scrollLeft = scrollLeft;
183
221
  }
184
222
  destroy(dom) {
223
+ previewWidgetObservers.get(dom)?.disconnect();
224
+ previewWidgetObservers.delete(dom);
185
225
  const component = previewWidgetMounts.get(dom);
186
226
  if (component) {
187
227
  void unmount(component);
@@ -46,8 +46,12 @@
46
46
  });
47
47
 
48
48
  $effect(() => {
49
+ const nextValue = value;
49
50
  if (editorEl?.ownerDocument.activeElement !== editorEl) {
50
- query = value;
51
+ query = nextValue;
52
+ if (editorEl && editorEl.textContent !== nextValue) {
53
+ editorEl.textContent = nextValue;
54
+ }
51
55
  }
52
56
  });
53
57
 
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter-property-text-input.svelte.d.ts","sourceRoot":"","sources":["../../../src/preview/components/frontmatter-property-text-input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAGH,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAI7B,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAoLJ,QAAA,MAAM,4BAA4B,2CAAwC,CAAC;AAC3E,KAAK,4BAA4B,GAAG,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC;AACpF,eAAe,4BAA4B,CAAC"}
1
+ {"version":3,"file":"frontmatter-property-text-input.svelte.d.ts","sourceRoot":"","sources":["../../../src/preview/components/frontmatter-property-text-input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAGH,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAI7B,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAwLJ,QAAA,MAAM,4BAA4B,2CAAwC,CAAC;AAC3E,KAAK,4BAA4B,GAAG,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC;AACpF,eAAe,4BAA4B,CAAC"}
@@ -451,6 +451,37 @@ describe("portable preview surfaces", () => {
451
451
  expect(controller.propertyManager.properties(controller.getRecord())[0]?.kind).toBe("number");
452
452
  await unmount(component);
453
453
  });
454
+ it("refreshes an edited text property after focus leaves the control", async () => {
455
+ const target = document.createElement("div");
456
+ document.body.append(target);
457
+ const controller = new FrontmatterController({
458
+ record: { status: "ready" },
459
+ });
460
+ const component = mount(FrontmatterEditor, {
461
+ target,
462
+ props: {
463
+ controller,
464
+ },
465
+ });
466
+ await settle();
467
+ const status = target.querySelector('[role="combobox"][aria-label="status value"]');
468
+ expect(status?.textContent).toBe("ready");
469
+ status?.focus();
470
+ expect(document.activeElement).toBe(status);
471
+ if (status)
472
+ status.textContent = "local";
473
+ status?.dispatchEvent(new InputEvent("input", { bubbles: true }));
474
+ controller.syncRecord({ status: "planned" }, { commit: false });
475
+ await settle();
476
+ expect(status?.textContent).toBe("local");
477
+ status?.blur();
478
+ await settle();
479
+ controller.syncRecord({ status: "review" }, { commit: false });
480
+ await settle();
481
+ expect(status?.textContent).toBe("review");
482
+ await unmount(component);
483
+ target.remove();
484
+ });
454
485
  it("keeps read-only list callouts passive and makes editable markers selectable", async () => {
455
486
  const readOnlyTarget = document.createElement("div");
456
487
  const editableTarget = document.createElement("div");
@@ -4,6 +4,8 @@ import rehypeRaw from "rehype-raw";
4
4
  import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
5
5
  import remarkEmoji from "remark-emoji";
6
6
  import remarkGfm from "remark-gfm";
7
+ import remarkMath from "remark-math";
8
+ import rehypeKatex from "rehype-katex";
7
9
  import { TYPE_TABLE, mdast2hastGridTablesHandler, } from "@adobe/mdast-util-gridtables";
8
10
  import { remarkHeadings, remarkWikiLinks } from "./remark";
9
11
  import { createParser } from "./renderer/utils";
@@ -13,6 +15,34 @@ describe("preview exports", () => {
13
15
  expect(typeof remarkWikiLinks).toBe("function");
14
16
  });
15
17
  });
18
+ describe("patched math renderer", () => {
19
+ it("renders inline and display math through the preview pipeline", () => {
20
+ const parser = createParser([remarkMath], [rehypeKatex]);
21
+ const ast = parser("Inline $x^2$.\n\n$$\nx^2 + y^2 = z^2\n$$");
22
+ const rendered = JSON.stringify(ast);
23
+ expect(rendered).toContain("katex-mathml");
24
+ expect(rendered).toContain("katex-display");
25
+ expect(findElement(ast, "math")).toBeDefined();
26
+ });
27
+ it("does not inherit polluted trust options when rendering authored math", () => {
28
+ const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, "trust");
29
+ try {
30
+ Object.defineProperty(Object.prototype, "trust", {
31
+ value: true,
32
+ configurable: true,
33
+ });
34
+ const parser = createParser([remarkMath], [rehypeKatex]);
35
+ const ast = parser("$\\href{https://example.com}{untrusted}$");
36
+ expect(findElement(ast, "a")).toBeUndefined();
37
+ }
38
+ finally {
39
+ if (descriptor)
40
+ Object.defineProperty(Object.prototype, "trust", descriptor);
41
+ else
42
+ Reflect.deleteProperty(Object.prototype, "trust");
43
+ }
44
+ });
45
+ });
16
46
  describe("Carta markdown parity", () => {
17
47
  const dataPng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
18
48
  it("renders reference-style base64 embedded images", () => {
@@ -2,13 +2,15 @@
2
2
  reading column, while preserving aspect ratio. Applies to both the
3
3
  dedicated image component and any raw <img> rendered through markdown
4
4
  (e.g. via rehype-raw HTML). Link-preview card thumbnails opt out since
5
- they already have their own, smaller sizing tuned for hover tooltips. */
5
+ they already have their own, smaller sizing tuned for hover tooltips.
6
+ CodeMirror cursor-buffer images must retain their zero-width inline layout
7
+ when an editor is nested inside rendered Markdown. */
6
8
  :is(
7
9
  .markdown-rendered,
8
10
  .markdown-preview-surface .markdown,
9
11
  .markdown-widget-shell
10
12
  )
11
- img:not(.mira-link-preview__asset),
13
+ img:not(.mira-link-preview__asset, .cm-widgetBuffer),
12
14
  .mira-markdown-image {
13
15
  border: 1px solid var(--mira-border);
14
16
  border-radius: var(--radius-s, 4px);
@@ -1,4 +1,4 @@
1
- .markdown-editor-surface .cm-editor {
1
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface) {
2
2
  cursor: text;
3
3
  font-family: var(--font-sans);
4
4
  font-size: var(--font-text-size);
@@ -37,25 +37,34 @@
37
37
  width: 0;
38
38
  }
39
39
 
40
- .markdown-editor-surface .cm-editor .cm-formatting-list-ul,
41
- .markdown-editor-surface .cm-editor .cm-formatting-list-ol {
40
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
41
+ .cm-formatting-list-ul,
42
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
43
+ .cm-formatting-list-ol {
42
44
  font-variant-numeric: tabular-nums;
43
45
  padding-inline-start: 0;
44
46
  }
45
47
 
46
- .markdown-editor-surface .cm-editor .cm-formatting-task {
48
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
49
+ .cm-formatting-task {
47
50
  color: var(--markdown-code-comment);
48
51
  }
49
52
 
50
- .markdown-live-preview-mode .cm-editor .cm-formatting-list-ul:not(.cm-line) {
53
+ :is(
54
+ .markdown-live-preview-mode .cm-editor,
55
+ .cm-editor.markdown-live-preview-mode
56
+ )
57
+ .cm-formatting-list-ul:not(.cm-line) {
51
58
  box-sizing: border-box;
52
59
  display: inline-block;
53
60
  inline-size: var(--hmd-unordered-list-marker-slot-width);
54
61
  padding-inline-start: var(--hmd-unordered-list-marker-padding-inline-start);
55
62
  }
56
63
 
57
- .markdown-live-preview-mode
58
- .cm-editor
64
+ :is(
65
+ .markdown-live-preview-mode .cm-editor,
66
+ .cm-editor.markdown-live-preview-mode
67
+ )
59
68
  .cm-formatting-list-bullet
60
69
  > span.cm-meta {
61
70
  color: transparent;
@@ -63,8 +72,10 @@
63
72
  position: relative;
64
73
  }
65
74
 
66
- .markdown-live-preview-mode
67
- .cm-editor
75
+ :is(
76
+ .markdown-live-preview-mode .cm-editor,
77
+ .cm-editor.markdown-live-preview-mode
78
+ )
68
79
  .cm-formatting-list-bullet
69
80
  > span.cm-meta::after {
70
81
  background-color: var(--list-marker-color);
@@ -82,13 +93,17 @@
82
93
  width: var(--list-bullet-size);
83
94
  }
84
95
 
85
- .markdown-live-preview-mode
86
- .cm-editor
96
+ :is(
97
+ .markdown-live-preview-mode .cm-editor,
98
+ .cm-editor.markdown-live-preview-mode
99
+ )
87
100
  .cm-line:hover
88
101
  .cm-formatting-list-bullet
89
102
  > span.cm-meta::after,
90
- .markdown-live-preview-mode
91
- .cm-editor
103
+ :is(
104
+ .markdown-live-preview-mode .cm-editor,
105
+ .cm-editor.markdown-live-preview-mode
106
+ )
92
107
  .cm-activeLine
93
108
  .cm-formatting-list-bullet
94
109
  > span.cm-meta::after {
@@ -96,11 +111,13 @@
96
111
  box-shadow: 0 0 0 4px var(--background-modifier-hover);
97
112
  }
98
113
 
99
- .markdown-editor-surface .cm-editor .HyperMD-list-line {
114
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
115
+ .HyperMD-list-line {
100
116
  white-space: pre-wrap;
101
117
  }
102
118
 
103
- .markdown-editor-surface .cm-editor .indented-wrapped-line {
119
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
120
+ .indented-wrapped-line {
104
121
  padding-inline-start: var(
105
122
  --hmd-indent-padding-measured,
106
123
  var(--hmd-indent-padding-fallback, 0px)
@@ -111,8 +128,7 @@
111
128
  );
112
129
  }
113
130
 
114
- .markdown-editor-surface
115
- .cm-editor
131
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
116
132
  .indented-wrapped-line:has(.cm-hmd-list-indent) {
117
133
  text-indent: calc(
118
134
  0px -
@@ -126,40 +142,54 @@
126
142
  );
127
143
  }
128
144
 
129
- .markdown-editor-surface .cm-editor .cm-line > * {
145
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
146
+ .cm-line
147
+ > * {
130
148
  text-indent: 0;
131
149
  }
132
150
 
133
- .markdown-editor-surface .cm-editor .cm-hmd-list-indent {
151
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
152
+ .cm-hmd-list-indent {
134
153
  display: inline-block;
135
154
  white-space: pre;
136
155
  }
137
156
 
138
- .markdown-editor-surface .cm-editor .cm-indent-guide {
157
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
158
+ .cm-indent-guide {
139
159
  min-width: var(--list-indent);
140
160
  }
141
161
 
142
- .markdown-editor-surface .cm-editor .cm-plain-indent-line,
143
- .markdown-editor-surface .cm-editor .cm-plain-indent-widget {
162
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
163
+ .cm-plain-indent-line,
164
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
165
+ .cm-plain-indent-widget {
144
166
  position: relative;
145
167
  }
146
168
 
147
- .markdown-editor-surface .cm-editor .cm-hmd-list-indent .cm-indent,
148
- .markdown-editor-surface .cm-editor .cm-plain-indent-widget .cm-indent {
169
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
170
+ .cm-hmd-list-indent
171
+ .cm-indent,
172
+ :is(.markdown-editor-surface .cm-editor, .cm-editor.markdown-editor-surface)
173
+ .cm-plain-indent-widget
174
+ .cm-indent {
149
175
  color: transparent;
150
176
  display: inline-block;
151
177
  position: relative;
152
178
  white-space: pre;
153
179
  }
154
180
 
155
- .markdown-editor-surface
156
- .cm-editor.cm-show-indentation-guides
181
+ :is(
182
+ .markdown-editor-surface .cm-editor.cm-show-indentation-guides,
183
+ .cm-editor.markdown-editor-surface.cm-show-indentation-guides
184
+ )
157
185
  .indented-wrapped-line {
158
186
  position: relative;
159
187
  }
160
188
 
161
- .markdown-editor-surface
162
- .cm-editor.cm-show-indentation-guides
189
+ :is(
190
+ .markdown-editor-surface .cm-editor.cm-show-indentation-guides,
191
+ .cm-editor.markdown-editor-surface.cm-show-indentation-guides
192
+ )
163
193
  .indented-wrapped-line::before {
164
194
  background-image: repeating-linear-gradient(
165
195
  to right,
@@ -181,8 +211,10 @@
181
211
  width: calc(var(--indent-guide-count, 0) * var(--indent-size, 4) * 1ch);
182
212
  }
183
213
 
184
- .markdown-editor-surface
185
- .cm-editor.cm-show-indentation-guides
214
+ :is(
215
+ .markdown-editor-surface .cm-editor.cm-show-indentation-guides,
216
+ .cm-editor.markdown-editor-surface.cm-show-indentation-guides
217
+ )
186
218
  .cm-activeLine.indented-wrapped-line::before {
187
219
  background-image: repeating-linear-gradient(
188
220
  to right,
@@ -208,12 +240,16 @@
208
240
  );
209
241
  }
210
242
 
211
- .markdown-editor-surface
212
- .cm-editor.cm-show-indentation-guides
243
+ :is(
244
+ .markdown-editor-surface .cm-editor.cm-show-indentation-guides,
245
+ .cm-editor.markdown-editor-surface.cm-show-indentation-guides
246
+ )
213
247
  .cm-hmd-list-indent
214
248
  .cm-indent-guide::before,
215
- .markdown-editor-surface
216
- .cm-editor.cm-show-indentation-guides
249
+ :is(
250
+ .markdown-editor-surface .cm-editor.cm-show-indentation-guides,
251
+ .cm-editor.markdown-editor-surface.cm-show-indentation-guides
252
+ )
217
253
  .cm-plain-indent-widget
218
254
  .cm-indent-guide::before {
219
255
  display: none;
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { readFileSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { cwd } from "node:process";
@@ -6,8 +7,22 @@ const sourceModeStyles = () => readFileSync(resolve(cwd(), "src/preview/styles/s
6
7
  describe("source mode styles", () => {
7
8
  it("reserves full guide slots only for complete indentation guide segments", () => {
8
9
  const styles = sourceModeStyles();
9
- expect(styles).toContain(".markdown-editor-surface .cm-editor .cm-indent-guide");
10
- expect(styles).toContain("min-width: var(--list-indent)");
11
- expect(styles).not.toMatch(/\.markdown-editor-surface\s+\.cm-editor\s+\.cm-indent\s*\{[^}]*min-width:\s*var\(--list-indent\)/u);
10
+ const guideRules = Array.from(styles.matchAll(/([^{}]+)\{([^{}]*)\}/gu))
11
+ .map((match) => ({ selector: match[1], declarations: match[2] }))
12
+ .filter(({ declarations }) => /min-width:\s*var\(--list-indent\)/u.test(declarations));
13
+ expect(guideRules).toHaveLength(1);
14
+ for (const markup of [
15
+ '<div class="markdown-editor-surface"><div class="cm-editor"><span class="cm-indent-guide"></span><span class="cm-indent"></span></div></div>',
16
+ '<div class="cm-editor markdown-editor-surface"><span class="cm-indent-guide"></span><span class="cm-indent"></span></div>',
17
+ ]) {
18
+ const host = document.createElement("div");
19
+ host.innerHTML = markup;
20
+ const guide = host.querySelector(".cm-indent-guide");
21
+ const partial = host.querySelector(".cm-indent");
22
+ for (const { selector } of guideRules) {
23
+ expect(guide.matches(selector.trim())).toBe(true);
24
+ expect(partial.matches(selector.trim())).toBe(false);
25
+ }
26
+ }
12
27
  });
13
28
  });
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@lapismd/mira",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Composable Markdown editor, extension SDK, preview renderer, and UI for Mira.",
5
5
  "author": "Steve Juma <help@lapis.md>",
6
- "license": "AGPL-3.0-or-later",
6
+ "license": "Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/lapismd/mira-mde.git",
@@ -86,6 +86,7 @@
86
86
  "bits-ui": "2.18.1",
87
87
  "clsx": "^2.1.1",
88
88
  "hast-util-to-text": "^4.0.2",
89
+ "katex": "0.18.10",
89
90
  "lodash-es": "^4.17.21",
90
91
  "mdast-util-from-markdown": "^2.0.2",
91
92
  "mdast-util-gfm-table": "^2.0.0",
@@ -152,8 +153,7 @@
152
153
  "@types/lodash-es": "^4.17.12",
153
154
  "@types/mdast": "^4.0.4",
154
155
  "@types/unist": "^3.0.3",
155
- "katex": "^0.16.22",
156
- "svelte": "^5.46.1"
156
+ "svelte": "^5.57.1"
157
157
  },
158
158
  "publishConfig": {
159
159
  "access": "public",