Skip to content
Draft
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
16 changes: 8 additions & 8 deletions packages/core/src/extensions/image-insert-caret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,25 @@ describe.each(['hide', 'focus'] as MarkMode[])(
},
)

// Regression: the image is NOT reordered (its contentDOM must stay before the
// preview so show mode reads `![alt](url)` then the preview). Confirm show mode
// keeps both the raw source and the preview, in that order.
describe('image show mode keeps source then preview', () => {
// The image mark view wraps the URL run, so its contentDOM (the URL) sits before
// the preview: in show mode the preview paints right after the URL, with the raw
// `![img](url)` still fully visible around it. Confirm show mode keeps both the
// raw source and the preview.
describe('image show mode paints the preview after the URL', () => {
it('shows the raw source and the preview together', async () => {
using fixture = setup('show', 'A![img](url)B')
await expect.element(preview).toBeVisible()

// The raw markdown stays visible (not collapsed) in show mode.
expect(fixture.dom.querySelector('p')?.innerText).toContain('![img](url)')

// The preview is painted after the source text, not inside or before it.
// The preview is painted next to the URL inside the still-visible source.
const previewRect = (preview.element() as HTMLElement).getBoundingClientRect()
const range = document.createRange()
const paragraph = fixture.dom.querySelector('p')!
range.selectNodeContents(paragraph)
// The trailing `B` is the last visible glyph; the preview sits before it but
// after the `![img](url)` text. A coarse check: the preview has real width and
// is within the paragraph's horizontal box.
// A coarse check: the preview has real width and is within the paragraph's
// horizontal box.
const paragraphRect = paragraph.getBoundingClientRect()
expect(previewRect.width).toBeGreaterThan(0)
expect(previewRect.left).toBeGreaterThanOrEqual(paragraphRect.left - 1)
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/extensions/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ function renderImagePreview(
}

/**
* Render `mdImageView` (anchored on the image's final character) as the inline
* image: the anchor char stays editable inside `contentDOM`, and the preview is
* a non-editable sibling. Mark-mode hides the surrounding `mdImageSource`, so
* what remains visible is the preview, in place of the raw `![alt](url)`.
* Render `mdImageView` (anchored on the image's URL run) as the inline image:
* the URL stays editable inside `contentDOM`, and the preview is a non-editable
* sibling painted right after it. Mark-mode hides the surrounding `mdImageSource`,
* so what remains visible is the preview, in place of the raw `![alt](url)`.
*/
function createImageMarkView(options: ImageOptions): MarkViewConstructor {
return (mark) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/extensions/inline-marks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { defineMarkSpec, union } from '@prosekit/core'
import type { MarkName } from './mark-names.ts'

/**
* Anchors an inline image preview on the final character of `![alt](url)`. A
* mark view (see `defineImage`) renders the image; without it the anchor char
* just renders as text. Carries the parsed `src`/`alt`.
* Anchors an inline image preview on the URL run of `![alt](url)` (the same
* range as `mdLinkUri`). A mark view (see `defineImage`) renders the image next
* to the URL; without it the URL just renders as text. Carries the parsed `src`/`alt`.
*/
function defineMdImageView() {
return defineMarkSpec<'mdImageView', MdImageViewAttrs>({
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/extensions/inline-text-to-mark-chunks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ describe('image', () => {
[4, 6] mdPack(key=image,data={"src":"http://x/p.png"}) + mdImageSource(src=http://x/p.png,alt=alt) + mdMark
[6, 9] mdPack(key=image,data={"src":"http://x/p.png"}) + mdImageSource(src=http://x/p.png,alt=alt)
[9, 11] mdPack(key=image,data={"src":"http://x/p.png"}) + mdImageSource(src=http://x/p.png,alt=alt) + mdMark
[11, 25] mdPack(key=image,data={"src":"http://x/p.png"}) + mdImageSource(src=http://x/p.png,alt=alt) + mdLinkUri
[25, 26] mdPack(key=image,data={"src":"http://x/p.png"}) + mdImageSource(src=http://x/p.png,alt=alt) + mdImageView(src=http://x/p.png,alt=alt) + mdMark
[11, 25] mdPack(key=image,data={"src":"http://x/p.png"}) + mdImageSource(src=http://x/p.png,alt=alt) + mdImageView(src=http://x/p.png,alt=alt) + mdLinkUri
[25, 26] mdPack(key=image,data={"src":"http://x/p.png"}) + mdImageSource(src=http://x/p.png,alt=alt) + mdMark
[26, 30]
"
`)
Expand All @@ -291,8 +291,8 @@ describe('image', () => {
expect(parse('![](z.png)')).toMatchInlineSnapshot(`
"
[0, 4] mdPack(key=image,data={"src":"z.png"}) + mdImageSource(src=z.png) + mdMark
[4, 9] mdPack(key=image,data={"src":"z.png"}) + mdImageSource(src=z.png) + mdLinkUri
[9, 10] mdPack(key=image,data={"src":"z.png"}) + mdImageSource(src=z.png) + mdImageView(src=z.png) + mdMark
[4, 9] mdPack(key=image,data={"src":"z.png"}) + mdImageSource(src=z.png) + mdImageView(src=z.png) + mdLinkUri
[9, 10] mdPack(key=image,data={"src":"z.png"}) + mdImageSource(src=z.png) + mdMark
"
`)
})
Expand All @@ -303,10 +303,10 @@ describe('image', () => {
[0, 2] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a) + mdMark
[2, 3] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a)
[3, 5] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a) + mdMark
[5, 13] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a) + mdLinkUri
[5, 13] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a) + mdImageView(src=http://x,alt=a) + mdLinkUri
[13, 14] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a)
[14, 17] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a) + mdLinkTitle
[17, 18] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a) + mdImageView(src=http://x,alt=a) + mdMark
[17, 18] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a) + mdMark
"
`)
})
Expand All @@ -321,8 +321,8 @@ describe('image', () => {
[7, 9] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdStrong + mdMark
[9, 11] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c)
[11, 13] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdMark
[13, 21] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdLinkUri
[21, 22] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdImageView(src=http://x,alt=a **b** c) + mdMark
[13, 21] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdImageView(src=http://x,alt=a **b** c) + mdLinkUri
[21, 22] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdMark
"
`)
})
Expand Down
19 changes: 8 additions & 11 deletions packages/core/src/extensions/inline-text-to-mark-chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,9 @@ function walkLink(
* Special walker for a direct image `![alt](url)`.
*
* Emits `mdImageSource` across the whole node (the mark `defineMarkMode` hides)
* and `mdImageView({ src, alt })` on the node's final character, which is the
* anchor a mark view renders the inline image on. The final character is `)`
* today and would be `]` for a future reference image `![alt][id]`, so the
* anchor is `node.to - 1`, never a hardcoded `)`. `mdMark`/`mdLinkUri` style the
* and `mdImageView({ src, alt })` over the `URL` run, the anchor a mark view
* renders the inline image on. The view wraps the same range as `mdLinkUri`, so
* the preview paints next to the URL it came from. `mdMark`/`mdLinkUri` style the
* source for show mode; the alt carries no `mdLinkText` (it is not a link), but
* inline emphasis inside it is still highlighted like any other syntax.
*/
Expand All @@ -262,15 +261,13 @@ function walkImage(
const view = marks.mdImageView.create({ src, alt } satisfies MdImageViewAttrs)
const pack = marks.mdPack.create({ key: `image`, data: { src } } satisfies MdPackAttrs)

// The image's final character, where `mdImageView` is anchored: `)` today, a
// future `]` for `![alt][id]`.
const anchorFrom = node.to - 1

// Marks shared by every chunk at `from`: the `mdPack` envelope plus
// `mdImageSource` over the whole source, plus `mdImageView` once we reach the
// final character (the render anchor). Each child layers its own syntax mark on top.
// `mdImageSource` over the whole source, plus `mdImageView` while inside the
// `URL` run (the render anchor, coincident with `mdLinkUri`). Each child layers
// its own syntax mark on top.
const inUrl = (from: number): boolean => from >= urlNode.from && from < urlNode.to
const baseAt = (from: number): Mark[] =>
from >= anchorFrom ? [...parentMarks, pack, source, view] : [...parentMarks, pack, source]
inUrl(from) ? [...parentMarks, pack, source, view] : [...parentMarks, pack, source]

let pos = node.from
for (const child of node.children) {
Expand Down
16 changes: 9 additions & 7 deletions packages/core/src/extensions/mark-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,18 +728,13 @@ describe('focus mode', () => {
](
</span>
</span>
<span class="md-link-uri">
<span class="show">
pic.png
</span>
</span>
</span>
<span class="md-image-view">
<span class="md-image-view-content">
<span class="md-image-source">
<span class="md-mark">
<span class="md-link-uri">
<span class="show">
)
pic.png
</span>
</span>
</span>
Expand All @@ -756,6 +751,13 @@ describe('focus mode', () => {
>
</span>
</span>
<span class="md-image-source">
<span class="md-mark">
<span class="show">
)
</span>
</span>
</span>
</span>
</p>
"
Expand Down
14 changes: 13 additions & 1 deletion packages/core/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -492,10 +492,22 @@
.md-mark,
.md-link-uri,
.md-link-title,
.md-image-source,
.md-wikilink-source {
display: none;
}

/* Keep the image source and its inner syntax chars in layout as zero-width,
* clipped boxes (their text is clipped to nothing, so only the preview shows)
* so a caret can stop on a source char that follows the preview, e.g. the
* trailing `)` or title now that `mdImageView` covers only the URL run. */
.md-image-source,
.md-image-source :is(.md-mark, .md-link-uri, .md-link-title) {
display: inline-flex;
width: 0px;
max-width: 0px;
min-width: 0px;
overflow: hidden;
}
}

.ProseMirror[data-mark-mode='focus'] {
Expand Down
Loading