@liminis/editor 0.4.1 → 0.6.0

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.
Files changed (41) hide show
  1. package/README.md +66 -1
  2. package/dist/app/editor/LinkClickPlugin.js +5 -1
  3. package/dist/app/editor/WikiLinkExistencePlugin.d.ts +13 -3
  4. package/dist/app/editor/WikiLinkExistencePlugin.js +91 -32
  5. package/dist/app/editor/editorNodes.js +3 -1
  6. package/dist/app/editor/nodes/BlockAnchorComponent.d.ts +5 -0
  7. package/dist/app/editor/nodes/BlockAnchorComponent.js +37 -0
  8. package/dist/app/editor/nodes/BlockAnchorNode.d.ts +42 -0
  9. package/dist/app/editor/nodes/BlockAnchorNode.js +151 -0
  10. package/dist/app/editor/nodes/CustomLinkNode.d.ts +14 -0
  11. package/dist/app/editor/nodes/CustomLinkNode.js +37 -0
  12. package/dist/app/editor/nodes/TransclusionComponent.d.ts +8 -0
  13. package/dist/app/editor/nodes/TransclusionComponent.js +65 -0
  14. package/dist/app/editor/nodes/TransclusionNode.d.ts +48 -0
  15. package/dist/app/editor/nodes/TransclusionNode.js +141 -0
  16. package/dist/app/editor/nodes/index.d.ts +4 -0
  17. package/dist/app/editor/nodes/index.js +2 -0
  18. package/dist/app/editor/nodes/transclusion-loading.d.ts +27 -0
  19. package/dist/app/editor/nodes/transclusion-loading.js +29 -0
  20. package/dist/app/editor/nodes/transclusion-render.d.ts +49 -0
  21. package/dist/app/editor/nodes/transclusion-render.js +186 -0
  22. package/dist/app/mapper/lexicalToMdast.js +116 -22
  23. package/dist/app/mapper/mdastToLexical.js +76 -4
  24. package/dist/host/defaults.js +1 -0
  25. package/dist/host/messages.d.ts +7 -1
  26. package/dist/host/messages.js +3 -3
  27. package/dist/host/types.d.ts +14 -1
  28. package/dist/markdown/parse.js +535 -1
  29. package/dist/markdown/stringify.js +35 -10
  30. package/dist/markdown/vendor/mdast-util-wiki-link/README.md +17 -0
  31. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +8 -1
  32. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +26 -1
  33. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +13 -7
  34. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +3 -1
  35. package/dist/styles.css +48 -0
  36. package/dist/types.d.ts +1 -0
  37. package/docs/decisions/adr-119-block-transclusion.md +247 -0
  38. package/docs/decisions/adr-122-block-anchor-badge.md +462 -0
  39. package/docs/editor-api.md +1 -0
  40. package/docs/markdown-pipeline.md +323 -6
  41. package/package.json +5 -3
@@ -199,17 +199,334 @@ inside the extension:
199
199
  `[[target]]` only because `parseMarkdown` substitutes a sentinel before parsing
200
200
  and sets `data._emptyAlias` after. The extensions alone produce no wiki-link
201
201
  node at all for that input.
202
-
203
- So: **if your pipeline enables GFM tables, or you care about `[[target|]]`, call
204
- `parseMarkdown` rather than assembling the extensions yourself.** Reach for the
205
- raw extensions only when you control the input and neither case applies.
202
+ - **Transclusion/embed detection (`![[target]]`, #119).** The `!`-prefixed form
203
+ is recognized only by `parseMarkdown`'s own embed-sentinel pre/post-processing
204
+ (see below) the raw extensions have no concept of it at all. A `!` before a
205
+ raw-extension `[[...]]` is just ordinary preceding text; the extensions never
206
+ produce a `wikiEmbed` node. `data.blockId` on a plain `wikiLink` node *is*
207
+ available through the raw extensions (it's pure token-value splitting inside
208
+ the vendored `mdast-util-wiki-link`, not pipeline-level surgery) — only the
209
+ embed marker itself is main-pipeline-only.
210
+
211
+ So: **if your pipeline enables GFM tables, cares about `[[target|]]`, or needs
212
+ `![[target#^id]]` transclusion detection, call `parseMarkdown` rather than
213
+ assembling the extensions yourself.** Reach for the raw extensions only when
214
+ you control the input and none of those cases apply.
206
215
 
207
216
  Note also that `<Editor>`'s own *serialization* does not go through
208
217
  `wikiLinkToMarkdown`: `stringifyMarkdown` carries a hand-rolled wiki-link handler
209
- that additionally understands `data._emptyAlias`. `wikiLinkToMarkdown` is the
210
- faithful vendored upstream serializer, not a byte-for-byte match for what the
218
+ that additionally understands `data._emptyAlias` and `wikiEmbed` nodes.
219
+ `wikiLinkToMarkdown` is the faithful vendored upstream serializer (extended with
220
+ `data.blockId` re-appending, see below), not a byte-for-byte match for what the
211
221
  editor emits.
212
222
 
223
+ ## Block-scoped links and transclusion (#119)
224
+
225
+ `[[file#^id]]` is a block-scoped link — the same `[[...]]` construct as
226
+ above, extended with an optional `#^blockId` fragment (Obsidian's
227
+ block-reference convention: a caret immediately after the `#`). It parses to
228
+ the same `wikiLink` node shape, with `data.blockId` set:
229
+
230
+ ```ts
231
+ { type: 'wikiLink', value: 'file', data: { alias, permalink, exists, blockId: 'id', /* … */ } }
232
+ ```
233
+
234
+ An ordinary heading anchor (`[[file#heading]]`, no caret) is untouched —
235
+ `data.blockId` is only ever set for the caret-prefixed form, so this is
236
+ purely additive to the existing anchor-link behavior described elsewhere in
237
+ this document.
238
+
239
+ `![[file#^id]]` — the same target+id addressing, `!`-prefixed — is
240
+ **transclusion**: a live, resolver-driven rendering of that block's current
241
+ content in place of the reference, not a link. It parses to a distinct
242
+ `wikiEmbed` node, not a `wikiLink` with a flag:
243
+
244
+ ```ts
245
+ { type: 'wikiEmbed', value: 'file', data: { alias, blockId: 'id', /* … */ } }
246
+ ```
247
+
248
+ `![[file]]` with no `#^id` fragment (whole-file transclusion) is not a
249
+ supported construct — the parser leaves it as an ordinary `[[file]]` link
250
+ (no embedding), never a `wikiEmbed`.
251
+
252
+ ### Why `wikiEmbed` is a separate node type, not a field on `wikiLink`
253
+
254
+ Every existing `wikiLink` consumer — including this repository's own
255
+ mdast↔Lexical mappers — is untouched by this addition. `[[file#^id]]` (link)
256
+ and `![[file#^id]]` (embed) are trivially distinguishable by `node.type` for
257
+ any downstream consumer, rather than requiring a new-field check added to
258
+ code that predates this feature.
259
+
260
+ ### How the embed marker is detected without a second vendored tokenizer
261
+
262
+ `micromark-extension-wiki-link` (unvendored, straight from npm) hooks only
263
+ the `[` character, with no `!`-prefix awareness. A literal `!` immediately
264
+ before `[[` is claimed *first* by the default CommonMark image-label-start
265
+ construct; when that construct fails to find a following `(url)`/`[ref]`
266
+ (which it always does for `[[target]]` — that isn't image syntax), bracket
267
+ resolution falls the *entire* `![[target]]` span back to one literal text
268
+ node, without the wiki-link tokenizer ever getting a chance to fire on the
269
+ inner `[[`.
270
+
271
+ Rather than vendoring a second tokenizer package to add `!`-prefix detection
272
+ (the LICENSE/provenance/parity-test burden this repository already paid once
273
+ for #347's `mdast-util-wiki-link` fix), `parseMarkdown` swaps a `!` for a
274
+ Private-Use-Area sentinel codepoint *before* parsing — but only when it is
275
+ immediately followed by a complete, single-line `[[...]]` span with no
276
+ internal `]`, exactly the grammar the tokenizer's own target/alias states
277
+ require to succeed. That condition is load-bearing, not incidental: a naive
278
+ "any `!` before `[[`" substitution collides with a real image whose alt text
279
+ starts with a literal bracket (`![[leading] bracket](img.png)` contains the
280
+ raw substring `![[`), and would prevent the image construct — which needs
281
+ the literal `!` — from ever being tried.
282
+
283
+ After parsing, a post-process retypes a sentinel-preceded `wikiLink` node to
284
+ `wikiEmbed` only when it carries a `blockId`; otherwise the literal `!` is
285
+ restored and the node stays an ordinary `wikiLink` (`![[file]]` with no id
286
+ degrades to a plain link, per the "not a supported construct" rule above).
287
+ The swap is one codepoint for one codepoint, so it needs no offset-remapping
288
+ the way the pipe-escaping/empty-alias-normalization pre-passes above do.
289
+
290
+ **If you are maintaining this package: do not remove or loosen the
291
+ "complete span, no internal `]`" condition on the embed-marker substitution
292
+ in `parse.ts`.** It looks like it could be simplified to a bare
293
+ `!(?=\[\[)` lookahead. Doing so silently corrupts any image whose alt text
294
+ starts with a bracketed span — caught before merge by the
295
+ `903-image-alt-leading-bracket` round-trip fixture, which is the regression
296
+ gate for this specific failure mode.
297
+
298
+ ### The transclusion resolver
299
+
300
+ `![[file#^id]]` renders the resolved block's live content via an optional
301
+ host-injected `resolveTransclusion(file, blockId) => Promise<string | null>`
302
+ (see `docs/editor-api.md`), consumed by a lazily-loaded component — not at
303
+ mapper time, since resolution is async and `mdastToLexical`/`lexicalToMdast`
304
+ are synchronous. With no resolver injected, or one that returns `null`, the
305
+ transclusion renders a clearly marked "unresolved" placeholder rather than
306
+ throwing or rendering nothing.
307
+
308
+ **If you are maintaining this package: do not remove the cycle/depth guard**
309
+ in `src/app/editor/nodes/transclusion-render.tsx`. A transclusion cycle (A
310
+ embeds B, B embeds A — directly, or transitively through a longer chain) is
311
+ guarded by a per-branch visited-path of `file#^blockId` keys, and nested
312
+ transclusion is bounded to a depth of 8, both checked *before* the resolver
313
+ is called at each level. Removing either turns an authoring mistake into an
314
+ infinite loop or unbounded recursion instead of a contained "circular
315
+ transclusion"/"nested too deeply" indicator.
316
+
317
+ ## Block anchor badges (#122, widened by #124 and #127)
318
+
319
+ A block anchor — a bare `^ULID` at its *definition* site, most commonly
320
+ trailing an action-item checkbox (`- [ ] ... ^01M00VDX0S4JHMDNA7F776Y8R8`) —
321
+ renders as a compact badge instead of the raw caret-plus-id text. This is
322
+ presentation only: the underlying markdown, and the id itself, never change.
323
+ It has no relationship to the `#^blockId` fragment inside `[[file#^id]]`
324
+ above other than sharing a source convention — this section is about the
325
+ anchor's own definition site, not a link's target.
326
+
327
+ ### `blockAnchor` is a new mdast node type, detected by a post-parse text split
328
+
329
+ ```ts
330
+ { type: 'blockAnchor', id: '01M00VDX0S4JHMDNA7F776Y8R8' }
331
+ ```
332
+
333
+ Unlike the embed marker (`![[...]]`), a bare `^ULID` needs no tokenizer
334
+ unlock — there is no bracket syntax to let through. Detection is therefore a
335
+ **post-parse** pass (`splitTextNodeBlockAnchors`/`splitBlockAnchors` in
336
+ `parse.ts`) walking already-typed mdast `text` nodes and splitting a matching
337
+ run into its own `blockAnchor` node, mirroring `#17`'s
338
+ `splitTextNodeEscapes`/`splitEscapedPunctuation` (including its decode-replay
339
+ position-mapping machinery and the same conservative bail-out: if a text
340
+ node's replayed decoding doesn't exactly reproduce `node.value` — e.g. a
341
+ character reference is present — the whole run is left unsplit rather than
342
+ risk a wrong split).
343
+
344
+ Because this only ever inspects a `text` node's own `value`, it structurally
345
+ cannot see into `inlineCode`, `code`, `inlineMath`, `wikiLink` or `wikiEmbed`
346
+ node content — none of those are `text` nodes once mdast has typed them —
347
+ which satisfies the code-span/fenced-code/math edge case and FR-006 for free,
348
+ with no "protected ranges" pre-parse machinery required (unlike
349
+ `substituteEmbedMarker` above, which needs that machinery only because it has
350
+ to influence tokenization itself).
351
+
352
+ The matcher also rejects a match whose leading `^` came from a backslash
353
+ escape (`\^`) in the source, using the same `replayDecodeEscapes` output the
354
+ decode-replay machinery above already computes. Without this check, an
355
+ author who deliberately wrote `\^` before a ULID-shaped run to mean literal
356
+ text — not an anchor — would still get a badge, and since the `blockAnchor`
357
+ stringify handler always emits a bare, unescaped `^id`, saving would silently
358
+ drop their backslash. This does not repair the pre-existing, unrelated gap
359
+ that `^` sits outside `FORCE_ESCAPE_CHARS`: a bare `\^` with no adjacent
360
+ ULID-shaped run still loses its backslash on round-trip today, anchor or
361
+ not — see ADR-122's accepted limitations.
362
+
363
+ The pass runs in `parseMarkdown`'s post-process sequence after
364
+ `resolveWikiEmbeds`/`annotateEmphasisMarkers` — so it never sees wiki-link or
365
+ embed target text — and immediately before `splitEscapedPunctuation` (which
366
+ stays last), so that pass still sees, and can process, any escaped
367
+ punctuation left in the anchor split's "before"/"after" text siblings.
368
+
369
+ `stringify.ts`'s `blockAnchor` handler emits exactly `^` + the node's `id`,
370
+ with no escaping — lossless by construction, the same way `wikiLink`/
371
+ `wikiEmbed` are.
372
+
373
+ ### The detection rule: one universal position rule, every id form alike (#124, #127, #126)
374
+
375
+ `findBlockAnchorMatches` in `parse.ts` applies a single rule at each
376
+ unescaped `^`, left to right: any id the resolver and the wiki-link
377
+ reference side already accept — ULID, raw-decimal snowflake ids, NanoID-style
378
+ ids with `_`/`-`, mixed-case base62, UUID-shaped hyphenated ids, short
379
+ alphanumeric ids — badges if and only if the caret starts a token (line-start
380
+ or preceded by whitespace) and the captured id (`WIDE_ID_CHAR`, the same
381
+ charset the wiki-link reference side and the resolver use) runs to end of
382
+ line. This is the resolver's own regex (`liminis-app/src/main/fs.ts`,
383
+ verveguy/liminis#1109) adopted verbatim, so badge and resolver agree by
384
+ construction rather than by coincidence, for every id form with no
385
+ carve-out. (At the time #124 shipped, `WIDE_ID_CHAR` was `[^\s\]#]+`; #127
386
+ later narrowed it to also exclude `*`, matching a further narrowing the
387
+ resolver itself picked up for `liminis#1114` — see below.)
388
+
389
+ **This was not always one rule.** #124 originally split detection into two
390
+ branches: ULID kept #122's original rule, unconstrained by position, while
391
+ every other id form was gated by the position rule above. That carve-out
392
+ existed because #122's own fixtures required a ULID to badge immediately
393
+ after a wiki-link/emphasis run with no preceding space, and required a ULID
394
+ followed by more prose on the same line to badge — shapes the resolver can
395
+ never address, since it only ever matches an anchor definition running to
396
+ end of line. #126 deleted the carve-out: those mid-line fixtures turned out
397
+ to encode parser-robustness tests, not a product requirement, because
398
+ production's actual mid-line ULID occurrences are wiki-link *references*
399
+ (`[[file#^id]]`), parsed position-independently and unaffected by this
400
+ change — not anchor *definitions*. A badged mid-line ULID was a promise
401
+ `[[file#^id]]` could never keep, the same "UI says X, system does Y"
402
+ disagreement #124 fixes, in the opposite direction. Deleting the carve-out
403
+ was a pure removal, not a rewrite: Crockford Base32 (ULID's charset) is
404
+ already a strict subset of the position rule's charset, so every ULID that
405
+ already satisfied the position rule — plain or emphasis-wrapped, at line
406
+ end — keeps badging unchanged; only mid-line ULIDs, and ULIDs immediately
407
+ adjacent to a preceding sibling with zero intervening whitespace, stopped
408
+ badging. See ADR-122's 2026-09-10 (#126) amendment for the full history,
409
+ including the rejected minimum-length threshold and the residual this left
410
+ (a caret with zero preceding whitespace, immediately after a wiki-link or
411
+ emphasis/strong sibling, can never badge at any line position — not just
412
+ mid-line).
413
+
414
+ Widening the charset without position-gating was, and remains, rejected:
415
+ `x^2`/`2^10`/`a ^ b` would badge as false positives, since nothing else
416
+ bounds an undelimited charset match the way `[[...]]` brackets bound the
417
+ wiki-link reference side's use of the same charset.
418
+
419
+ A shared, cross-repo case table
420
+ (`src/markdown/__tests__/blockAnchorCases.ts`, `BLOCK_ANCHOR_POSITION_CASES`)
421
+ pins this rule's id/position behavior against the resolver's actual, merged
422
+ `ANCHOR_LINE_PATTERN`, asserted in full by a single `it.each` in
423
+ `parse.test.ts` — so a future divergence between the two repositories' rules
424
+ fails a test instead of shipping as a live defect, the way both #124's and
425
+ #126's own gaps originally did.
426
+
427
+ The boundary checks may need to peek one character outside the current text
428
+ node's own source span — into the surrounding normalized document text, one
429
+ character before the node's start or from the node's end onward — since a
430
+ preceding/following sibling (e.g. a wiki-link) has no character of its own
431
+ for the matcher to inspect directly. This is equivalent to inspecting the
432
+ sibling node and needs no AST traversal to do it.
433
+
434
+ **A symmetric emphasis wrapper at line end (added by #127).**
435
+ `**^a1b2c3**`, `__^a1b2c3__`, `*^a1b2c3*` and `_^a1b2c3_` all badge
436
+ `a1b2c3` — matching `verveguy/liminis#1114`'s widened resolver, which
437
+ accepts the same wrapped shape.
438
+
439
+ When CommonMark parses `**^a1b2c3**` as real emphasis, the wrapper
440
+ characters are consumed into the *parent* `strong`/`emphasis` node's
441
+ position span — they never appear in the inner `text` node's own
442
+ `decoded`/`value`. That has a useful structural consequence: a caret can
443
+ only be adjacent to a *structural* wrapper marker when it is the first
444
+ character of its own text node (`i === 0`), and a closer only when the id
445
+ capture runs all the way to that same node's own end
446
+ (`idEnd === decoded.length`) — outside those two positions, whatever
447
+ wrapper-like characters are present are literal text left over from an
448
+ unmatched delimiter run, already correctly rejected by the plain rule with
449
+ no wrapper logic involved. Gating wrapper detection on both invariants
450
+ means the extension is just two more raw-character peeks into the
451
+ surrounding normalized text (`matchWrapperMarkerBefore` on the open side,
452
+ an exact-string check on the close side), the same style the plain
453
+ whitespace boundary above already uses — no need to pass the enclosing
454
+ `strong`/`emphasis` node's type, marker, or position down through the tree
455
+ walk.
456
+
457
+ The wrapped id charset (`WRAPPED_ID_CHAR`, `[^\s\]#*]`) matches the
458
+ resolver's actual wrapped-branch charset as implemented for `liminis#1114`
459
+ (`ANCHOR_LINE_PATTERN`'s `[^\s\]#*]+?` wrapped-id group) — it excludes `*`
460
+ but allows `_`, since `_` is a legitimate character in ids like
461
+ `V1StGXR8_Z5jdHi6B-myT` (NanoID) and `snake_case_id`. `WIDE_ID_CHAR` (the
462
+ unwrapped path) uses the same charset: the resolver's implemented pattern
463
+ narrowed *both* its wrapped and unwrapped branches to exclude `*`, not just
464
+ the wrapped one, so the two constants are currently identical — kept as
465
+ separate names since they're independent knobs in the resolver's pattern
466
+ that could diverge again. Neither excludes `_`: doing so would regress the
467
+ NanoID-with-underscore case (`^V1StGXR8_Z5jdHi6B-myT`, #124/FR-004), and the
468
+ resolver doesn't exclude it either.
469
+
470
+ The closer must be the *exact same marker string* that opened it — not an
471
+ independently optional match — so an asymmetric wrapper (`item **^<ULID>_`)
472
+ is rejected outright rather than captured with a corrupted id, ULID or not
473
+ (#126: there is no longer a position-free carve-out for it to fall back to).
474
+ Wrapper forms other than `**`/`__`/`*`/`_` — triple emphasis (`***…***`),
475
+ strikethrough (`~~…~~`), backtick-wrapped, and paren-wrapped — stay
476
+ unhandled by design: `WRAPPER_MARKERS` only lists the four forms
477
+ `liminis#1114`'s resolver pattern accepts. `squared *^2*` becomes a
478
+ badged-and-resolved false anchor as a result of this widening — accepted
479
+ deliberately, for the same reason a minimum-length floor was already
480
+ rejected for `^100` in #124 (excluding it would also exclude legitimate
481
+ short ids like `^a1b2c3`). See ADR-122's 2026-09-10 (#127) and 2026-09-10
482
+ (#126) amendments.
483
+
484
+ ### The Lexical side: `BlockAnchorNode`, mirroring `FootnoteNode`
485
+
486
+ `BlockAnchorNode` (`src/app/editor/nodes/BlockAnchorNode.tsx`) is an inline
487
+ `DecoratorNode<JSX.Element>` carrying the id, a format bitmask, and
488
+ strong/emphasis marker fields — the same shape as `FootnoteNode`, so an
489
+ anchor sitting inside `**bold**`/`_italic_` still round-trips its original
490
+ marker style rather than silently dropping it (the defect class already
491
+ fixed once for other inline decorators, `#898`/`#908`). `BlockAnchorComponent`
492
+ renders the actual badge: the full id is exposed via the native `title`
493
+ tooltip on hover, and a click copies it via `navigator.clipboard.writeText`
494
+ with brief "Copied" feedback — mirroring `CodeBlockPlugin.tsx`'s existing
495
+ copy pattern, rather than a new interaction affordance (User Story 2/SC-003).
496
+
497
+ `convertInlineNode` (`mdastToLexical.ts`) maps a `blockAnchor` mdast node to
498
+ `$createBlockAnchorNode`; the reverse direction in `lexicalToMdast.ts` adds an
499
+ `$isBlockAnchorNode` branch at every site that already special-cases
500
+ `$isFootnoteNode` (`isHoistableConstruct`, `getMergeableFormat`,
501
+ `resolveMarkers`, and each content-conversion call site) so a `BlockAnchorNode`
502
+ participates in bold/italic wrapping, mark-boundary hoisting, and format
503
+ merging exactly like every other round-trip-sensitive inline construct.
504
+
505
+ **If you are maintaining this package: `convertListItemNode` keeps a
506
+ `BlockAnchorNode` inline, unlike an image/equation/footnote/inline-HTML
507
+ child of a list item** (a documented, pre-existing gap — see
508
+ `hoistedTokenReachesOutput`'s docstring in `lexicalToMdast.ts` — where those
509
+ constructs fall to the block dispatcher and do not survive a round trip
510
+ inline). `- [ ] ... ^ULID` is this feature's primary real-world shape, so
511
+ dropping a trailing anchor there would break the checkbox-action-item
512
+ pattern the spec calls out explicitly. Do not fold the `$isBlockAnchorNode`
513
+ branch back into the generic block-dispatch fallback.
514
+
515
+ ### Round-trip contract
516
+
517
+ `parseMarkdown` → `stringifyMarkdown` reproduces a document containing block
518
+ anchors byte-identically (FR-002/SC-002), including inside a checkbox action
519
+ item, one space after a wiki-link or emphasis run, and inside bold/italic
520
+ text. A caret inside inline code, a fenced code block, or inline math is
521
+ never touched — it round-trips as plain literal text, since the post-parse
522
+ pass never sees inside those node types. A caret with *zero* preceding
523
+ whitespace immediately after a wiki-link or emphasis run also round-trips
524
+ byte-identically, but as plain text, not a `blockAnchor` node (#126: that
525
+ position can never satisfy the position rule, so it never badges — see the
526
+ detection-rule section above). See
527
+ `src/app/mapper/__tests__/fixtures/roundtrip/122-block-anchor/` for the
528
+ fixture corpus.
529
+
213
530
  ## Wiki-link promotion on export
214
531
 
215
532
  Everything above is about *parsing* `[[target]]` syntax the author already wrote.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liminis/editor",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "//publishing": "Publishing is deliberate, never incidental. `private: true` was this package's guard until verveguy/liminis-editor#39 took the publish decision; it is gone because that decision was taken, not because it was tidied away. The guard is now `prepublishOnly` -> scripts/guard-publish.mjs, which refuses unless LIMINIS_ALLOW_PUBLISH=1 is set explicitly. That variable is set at step scope in .github/workflows/publish.yml and nowhere else, so a release is the only path that publishes. Note that `npm publish --dry-run` does NOT report a private package as blocked (npm 10.8.2), which is why the guard is a script rather than a flag.",
5
5
  "license": "MIT",
6
6
  "description": "Lexical-based markdown WYSIWYG editor with mdast round-trip and a host-injection seam",
@@ -74,14 +74,15 @@
74
74
  "test:watch": "vitest",
75
75
  "test:coverage": "vitest run --coverage",
76
76
  "verify:package": "node scripts/verify-package.mjs",
77
+ "check:drift": "node scripts/check-lockfile-drift.mjs",
77
78
  "docs:theming": "node scripts/generate-theming-docs.mjs",
78
79
  "docs:theming-baseline": "node scripts/update-theming-baseline.mjs",
79
80
  "demo": "node scripts/run-demo.mjs",
80
81
  "build:examples": "node scripts/build-examples.mjs",
81
82
  "build:site": "node scripts/build-site.mjs"
82
83
  },
83
- "//peerDependencies": "Ranges are the compatibility contract, not the tested matrix. CI resolves one point version per range (currently react 19.2.x latest and lexical 0.49.x latest), so every caret range here has an untested lower half \u2014 narrowing `react` to the devDependency's `^19.2.5` would not change that, it would only reject working consumers. The react floor is not a claim, it is enforced: `tests/package-manifest-contract.test.ts` (\"imports no React API newer than the declared 19.2.0 peer floor\") scans every source file for post-19.0 named imports \u2014 `useEffectEvent`, `Activity`, `cacheSignal`, `ViewTransition`, `captureOwnerStack` \u2014 and fails if one appears. Reaching for a newer API therefore breaks CI until you raise the floor here too, which is the pairing this note would otherwise only assert. Raise a floor when the code actually starts needing a later API, not to mirror whatever CI happened to install. This reasoning does not extend to the Lexical ranges below \u2014 see `//lexicalPeerPolicy`.",
84
- "//lexicalPeerPolicy": "The twelve Lexical ranges are a single-caret bump per Lexical minor actually adopted and tested here, not a wide multi-version band. `react`'s untested lower half (above) is safe because a break there would be a bug in a stable major; Lexical is `0.x`, where every minor is permitted to break, so a wide band would claim compatibility with untested, potentially-breaking releases. Widening a range later is patch-safe; narrowing one is breaking \u2014 so this stays narrow by default and moves forward only when the code is actually run against the new minor. See docs/decisions/adr-92-lexical-peer-range-policy.md.",
84
+ "//peerDependencies": "Ranges are the compatibility contract, not the tested matrix. CI resolves one point version per range (currently react 19.2.x latest and lexical 0.49.x latest), so every caret range here has an untested lower half narrowing `react` to the devDependency's `^19.2.5` would not change that, it would only reject working consumers. The react floor is not a claim, it is enforced: `tests/package-manifest-contract.test.ts` (\"imports no React API newer than the declared 19.2.0 peer floor\") scans every source file for post-19.0 named imports `useEffectEvent`, `Activity`, `cacheSignal`, `ViewTransition`, `captureOwnerStack` and fails if one appears. Reaching for a newer API therefore breaks CI until you raise the floor here too, which is the pairing this note would otherwise only assert. Raise a floor when the code actually starts needing a later API, not to mirror whatever CI happened to install. This reasoning does not extend to the Lexical ranges below see `//lexicalPeerPolicy`.",
85
+ "//lexicalPeerPolicy": "The twelve Lexical ranges are a single-caret bump per Lexical minor actually adopted and tested here, not a wide multi-version band. `react`'s untested lower half (above) is safe because a break there would be a bug in a stable major; Lexical is `0.x`, where every minor is permitted to break, so a wide band would claim compatibility with untested, potentially-breaking releases. Widening a range later is patch-safe; narrowing one is breaking so this stays narrow by default and moves forward only when the code is actually run against the new minor. See docs/decisions/adr-92-lexical-peer-range-policy.md.",
85
86
  "peerDependencies": {
86
87
  "@lexical/code": "^0.49.0",
87
88
  "@lexical/code-prism": "^0.49.0",
@@ -150,6 +151,7 @@
150
151
  "lexical": "^0.49.0",
151
152
  "react": "^19.2.5",
152
153
  "react-dom": "^19.2.5",
154
+ "semver": "^7.8.5",
153
155
  "tsc-alias": "^1.9.1",
154
156
  "typescript": "^6.0.3",
155
157
  "typescript-eslint": "^8.59.0",