Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
"labels": ["security", "dependencies"]
},
"packageRules": [
{
"description": "Direct Lexical imports must share MDXEditor's installed modules (docs/specs/dor-tools-builtin.md -> Markdown editor). Update these exact pins in the MDXEditor upgrade PR when its Lexical dependencies change; independent bumps can install a second copy even when grouped together.",
"matchManagers": ["npm"],
"matchFileNames": ["dor-tools-builtin/package.json"],
"matchPackageNames": ["lexical", "@lexical/**"],
"enabled": false
},
{
"description": "The built-in Markdown editor stays on Mermaid 11; see docs/specs/dor-tools-builtin.rationale.md, Markdown editor",
"matchPackageNames": ["mermaid"],
Expand Down
1 change: 1 addition & 0 deletions docs/specs/dor-tools-builtin.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Source of truth: `saveText` / `StorePath` in `dor-tools-builtin/src/editor-route

`.md` and `.markdown` open in the bundled MDXEditor page; `.mdx` and other text stay in Monaco. Saving, reloading, and dirty reports follow [Editing files](#editing-files).

- **Must resolve the viewer's direct `lexical` and `@lexical/link` imports to the same installed modules MDXEditor uses** (rationale).
- **Must support GFM tables, task lists, frontmatter, code blocks, fenced `mermaid` diagrams, and a whole-document source mode**, which opens with the error when the rich editor cannot parse a file. Mermaid stays on 11 (rationale).
- **Must keep HTML comments verbatim and save with one final newline, the file's majority bullet and thematic-break marker, and unpadded tables**; the rest follows MDXEditor's serialization. Its normalization of loaded text is not an edit (rationale).
- **Never let document HTML reach the live DOM outside an allowlist** of tags and attributes, `href` limited to `http(s):`, `mailto:`, fragments, and scheme-less paths; saving keeps every original attribute, escaped, and HTML images serialize in an inert document. Mermaid diagrams are the one other path, under mermaid's own sanitizer (`docs/specs/security-local.md` → "Local-file viewer"; rationale).
Expand Down
2 changes: 2 additions & 0 deletions docs/specs/dor-tools-builtin.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Without a title, a viewer's header falls back to its running command, `dor __vie

## Markdown editor

MDXEditor 4.3.3 depends on Lexical `^0.48.0`; independent direct bumps to 0.52 install a second copy (PRs #1195 and #1205, 2026-10). Lexical's active editor state and node classes belong to its module instance, and the viewer's relative-link patch changes `LinkNode.prototype`, so even a successful build can leave editor helpers using the wrong state or patching a class MDXEditor never instantiates. Renovate leaves these direct pins to the MDXEditor upgrade PR; grouping alone does not select compatible versions.

MDXEditor 4.3 parses `<!-- … -->` and discards it, and trims and re-serializes the whole document on export; without the comment node and style detection, saving a typical README after a one-word edit deleted its comments and rewrote every `-` bullet as `*` (measured 2026-10).

Image sizes live in the document because GitHub shows an `<img>` without a `width` pixel for pixel: a Retina screenshot shown here at its recorded scale but left unsized in the file came out twice as large on GitHub. Writing the width on paste keeps the two renderings alike (2026-10).
Expand Down
22 changes: 22 additions & 0 deletions dor-tools-builtin/test/markdown-dependencies.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { test } from 'node:test';

const viewer = createRequire(new URL('../viewer/markdown.tsx', import.meta.url));
const editor = createRequire(viewer.resolve('@mdxeditor/editor'));

// Compare installed module identities, not just declared ranges or versions:
// the editor state, node classes, and LinkNode prototype patch must be shared.
// When MDXEditor moves to another Lexical release, update our direct pins in
// that same PR. This runs in the package's normal test suite and root CI.
for (const name of ['lexical', '@lexical/link']) {
test(`the Markdown viewer and MDXEditor share ${name}`, () => {
assert.equal(viewer.resolve(name), editor.resolve(name),
`Update the direct ${name} pin with @mdxeditor/editor; separate copies break the Markdown editor`);
});
}

test('the viewer LinkNode uses the editor’s Lexical state', () => {
const link = createRequire(viewer.resolve('@lexical/link'));
assert.equal(link.resolve('lexical'), editor.resolve('lexical'));
});
6 changes: 4 additions & 2 deletions scripts/spec-lint-selftest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ const CASES = [
['check 8: an external rationale key with no heading', EXTERNAL_RATIONALE, '\n## No matching heading\n'],
['check 13: a missing heading in an external spec', SOURCE, `\n// ${EXTERNAL_SPEC} -> "No Such Heading"\n`],
['check 4: a repo path that does not exist', SPEC, '\nSee `lib/src/no-such-file.ts`.\n'],
['check 11: a (rationale) marker under a heading the rationale does not key', SPEC, '\n## Planted\n\nA rule (rationale).\n'],
['check 11: the marker as the last item of its parenthetical', SPEC, '\n## Planted\n\nA rule (see below; rationale).\n'],
// Start a new root: a rationale keyed to the original H1 also covers every
// appended H2, so that would not plant a defect in specs such as security.md.
['check 11: a (rationale) marker under a heading the rationale does not key', SPEC, '\n# Planted\n\nA rule (rationale).\n'],
['check 11: the marker as the last item of its parenthetical', SPEC, '\n# Planted\n\nA rule (see below; rationale).\n'],
['check 12: a bare file name in Source of truth', SPEC, '\nSource of truth: `lint-kit.mjs`.\n'],
['check 12: a bare file name under a punctuated lead-in', SPEC, '\nSource of truth, all in `lib/src/lib/`: `Wall.tsx`.\n'],
['check 12: a symbol the named file lacks', SPEC, '\nSource of truth: `noSuchSymbolXyz` in `scripts/lint-kit.mjs`.\n'],
Expand Down
Loading