import { describe, expect, it } from 'vitest' import { imageMarkdownSpansIn } from '@radial/core' import { afterContainingImage, remap } from './insertion.js' const URI = 'at://did:plc:abc/com.disnetdev.radial.image/3mrv' describe('remap', () => { it('leaves a position before the edit where it was', () => { expect(remap('one two', 'one THREE two', 3)).toBe(3) expect(remap('one two', 'one THREE two', 0)).toBe(0) }) it('moves a position after the edit by what the edit added or removed', () => { expect(remap('one two', 'one THREE two', 7)).toBe(13) expect(remap('one THREE two', 'one two', 13)).toBe(7) }) it('associates forward, so text typed AT the point lands before it', () => { // This is the rule that makes a batch come out in order: the image this component just wrote // leaves the anchor after itself, ready for the next file of the same drop. expect(remap('ab', 'aXb', 1)).toBe(2) expect(remap('', `![one](radial-image:${URI})`, 0)).toBe(`![one](radial-image:${URI})`.length) }) it('collapses a position inside deleted text to where the deletion ends', () => { expect(remap('a-DELETED-b', 'a-b', 6)).toBe(2) }) it('lands at the end of a document the parent replaced wholesale', () => { // A composer clears its draft after a successful post. The upload still has to go somewhere, and // the end of what is there now is the only honest answer. expect(remap('a message', '', 4)).toBe(0) expect(remap('a message', 'a different draft', 4)).toBe('a different draft'.length) }) it('takes a position outside the document and an unchanged one as they are', () => { expect(remap('abc', 'abc', 2)).toBe(2) expect(remap('abc', 'abcd', 99)).toBe(4) expect(remap('abc', 'abcd', -1)).toBe(0) }) }) describe('afterContainingImage', () => { const one = `![one](radial-image:${URI})` it('pushes a caret parked inside an image past the end of it', () => { // The editor selects the alt so the reader can correct it; typing over that selection leaves the // caret between `![` and `](`, which is the one place the next image must not go. expect(afterContainingImage(one, 3, 3)).toBe(one.length) expect(afterContainingImage(`tail ${one} more`, 8, 8)).toBe(5 + one.length) }) it('leaves a caret at either edge of an image alone', () => { expect(afterContainingImage(one, 0, 0)).toBeUndefined() expect(afterContainingImage(one, one.length, one.length)).toBeUndefined() }) it('leaves ordinary prose, and ordinary markdown images, alone', () => { expect(afterContainingImage('nothing here at all', 4, 4)).toBeUndefined() expect(afterContainingImage('![a cat](https://example.test/cat.png)', 4, 4)).toBeUndefined() }) it('finds the image a selection runs into, whichever end is inside it', () => { expect(afterContainingImage(`${one} tail`, one.length - 2, one.length + 3)).toBe(one.length) expect(afterContainingImage(`tail ${one}`, 2, 8)).toBe(5 + one.length) }) it('reports the second image when that is the one the caret is in', () => { const two = `![two](radial-image:${URI})` expect(afterContainingImage(one + two, one.length + 3, one.length + 3)).toBe( one.length + two.length, ) }) it('uses the core locator parser, including its case-insensitive scheme', () => { const mixed = `![one](RADIAL-IMAGE:${URI})` expect(imageMarkdownSpansIn(mixed)).toEqual([{ from: 0, to: mixed.length }]) expect(afterContainingImage(mixed, 3, 3)).toBe(mixed.length) expect(imageMarkdownSpansIn('![one](radial-image:https://example.test/not-a-record)')).toEqual([]) }) })