From c83c4d0780a08a5363f71f092023dc1bf716f16b Mon Sep 17 00:00:00 2001
From: ocavue
Date: Sat, 27 Jun 2026 20:23:05 +1000
Subject: [PATCH 1/4] refactor image view mark to wrap uri
---
packages/core/src/extensions/image.ts | 8 +++----
packages/core/src/extensions/inline-marks.ts | 5 ++--
.../inline-text-to-mark-chunks.test.ts | 16 ++++++-------
.../extensions/inline-text-to-mark-chunks.ts | 23 +++++++++---------
.../core/src/extensions/mark-mode.test.ts | 16 +++++++------
packages/core/src/style.css | 24 +++++++++++++------
6 files changed, 51 insertions(+), 41 deletions(-)
diff --git a/packages/core/src/extensions/image.ts b/packages/core/src/extensions/image.ts
index dd2bf762..05383962 100644
--- a/packages/core/src/extensions/image.ts
+++ b/packages/core/src/extensions/image.ts
@@ -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 ``.
+ * Render `mdImageView` (wrapped around the image URI) as the inline image: the
+ * URI 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 ``.
*/
function createImageMarkView(options: ImageOptions): MarkViewConstructor {
return (mark) => {
diff --git a/packages/core/src/extensions/inline-marks.ts b/packages/core/src/extensions/inline-marks.ts
index 8efb90b9..6b9acbd6 100644
--- a/packages/core/src/extensions/inline-marks.ts
+++ b/packages/core/src/extensions/inline-marks.ts
@@ -3,9 +3,8 @@ import { defineMarkSpec, union } from '@prosekit/core'
import type { MarkName } from './mark-names.ts'
/**
- * Anchors an inline image preview on the final character of ``. A
- * mark view (see `defineImage`) renders the image; without it the anchor char
- * just renders as text. Carries the parsed `src`/`alt`.
+ * Wraps the parsed URI of ``. A mark view (see `defineImage`) renders
+ * the image; without it the URI renders as text. Carries the parsed `src`/`alt`.
*/
function defineMdImageView() {
return defineMarkSpec<'mdImageView', MdImageViewAttrs>({
diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts
index ce038f24..8541b4c2 100644
--- a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts
+++ b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts
@@ -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]
"
`)
@@ -291,8 +291,8 @@ describe('image', () => {
expect(parse('')).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
"
`)
})
@@ -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
"
`)
})
@@ -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
"
`)
})
diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.ts
index 61d9839a..78e4dfbf 100644
--- a/packages/core/src/extensions/inline-text-to-mark-chunks.ts
+++ b/packages/core/src/extensions/inline-text-to-mark-chunks.ts
@@ -233,12 +233,10 @@ function walkLink(
* Special walker for a direct image ``.
*
* 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
- * 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.
+ * and `mdImageView({ src, alt })` across the parsed URL range, where the mark
+ * view renders the inline image. `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.
*/
function walkImage(
node: InlineElement,
@@ -262,15 +260,16 @@ 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
+ const viewFrom = urlNode.from
+ const viewTo = urlNode.to
+ const sourceMarks = [...parentMarks, pack, source]
+ const viewMarks = [...sourceMarks, view]
// 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` across the URL
+ // range. Each child layers its own syntax mark on top.
const baseAt = (from: number): Mark[] =>
- from >= anchorFrom ? [...parentMarks, pack, source, view] : [...parentMarks, pack, source]
+ from >= viewFrom && from < viewTo ? viewMarks : sourceMarks
let pos = node.from
for (const child of node.children) {
diff --git a/packages/core/src/extensions/mark-mode.test.ts b/packages/core/src/extensions/mark-mode.test.ts
index 97bbffbf..25d18b10 100644
--- a/packages/core/src/extensions/mark-mode.test.ts
+++ b/packages/core/src/extensions/mark-mode.test.ts
@@ -728,18 +728,13 @@ describe('focus mode', () => {
](
-
-
- pic.png
-
-
-
+
- )
+ pic.png
@@ -756,6 +751,13 @@ describe('focus mode', () => {
>
+
+
+
+ )
+
+
+
"
diff --git a/packages/core/src/style.css b/packages/core/src/style.css
index 0ab0df48..fa84be64 100644
--- a/packages/core/src/style.css
+++ b/packages/core/src/style.css
@@ -550,14 +550,24 @@
}
}
-/* The trailing source char inside a view contentDOM is hidden by the general
- * `display:none` above; that leaves it without a box, so the caret falls back
- * before it. Put it back in the layout (kept invisible by the transparent/clip
- * rules above) so the caret has a real stop right after the unit. */
+/* Source inside a view contentDOM is hidden by the general `display:none` above;
+ * that leaves it without a box, so the caret falls back before it. Put it back
+ * in the layout when hidden so the caret has a real stop right after the unit. */
.ProseMirror[data-mark-mode='hide'] .md-wikilink-view-content :is(.md-mark, .md-wikilink-source),
-.ProseMirror[data-mark-mode='focus'] .md-wikilink-view-content :is(.md-mark, .md-wikilink-source),
-.ProseMirror[data-mark-mode='hide'] .md-image-view-content :is(.md-mark, .md-image-source),
-.ProseMirror[data-mark-mode='focus'] .md-image-view-content :is(.md-mark, .md-image-source) {
+.ProseMirror[data-mark-mode='focus'] .md-wikilink-view-content :is(.md-mark, .md-wikilink-source) {
+ display: inline-flex;
+ width: 0px;
+ max-width: 0px;
+ min-width: 0px;
+ overflow: visible;
+}
+
+.ProseMirror[data-mark-mode='hide']
+ .md-image-view-content
+ :is(.md-mark, .md-link-uri, .md-image-source),
+.ProseMirror[data-mark-mode='focus']
+ .md-image-view-content:not(:has(.show))
+ :is(.md-mark, .md-link-uri, .md-image-source) {
display: inline-flex;
width: 0px;
max-width: 0px;
From 368fd3813853bd7c54fa2dae95be4cc9f67ada4b Mon Sep 17 00:00:00 2001
From: ocavue
Date: Sat, 27 Jun 2026 20:25:55 +1000
Subject: [PATCH 2/4] chore: trigger ci
From 75aa3c89f14cd191c8407791da84bf2220b7cf41 Mon Sep 17 00:00:00 2001
From: ocavue
Date: Sat, 27 Jun 2026 20:41:28 +1000
Subject: [PATCH 3/4] fix
---
packages/core/src/extensions/image.ts | 11 ++++++++++-
website/src/app.tsx | 4 +---
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/extensions/image.ts b/packages/core/src/extensions/image.ts
index 05383962..3e9f991e 100644
--- a/packages/core/src/extensions/image.ts
+++ b/packages/core/src/extensions/image.ts
@@ -98,10 +98,19 @@ function createImageMarkView(options: ImageOptions): MarkViewConstructor {
dom.appendChild(preview)
}
+ console.log('creating image mark view', attrs.src, attrs.alt, preview)
+
return {
dom,
contentDOM,
- ignoreMutation: (mutation) => !contentDOM.contains(mutation.target),
+ ignoreMutation: (mutation) => {
+ let result = !contentDOM.contains(mutation.target)
+ console.log('ignoreMutation', result, mutation)
+ return false
+ },
+ destroy: () => {
+ console.log('destroying image mark view')
+ },
}
}
}
diff --git a/website/src/app.tsx b/website/src/app.tsx
index 6139a92f..2629fa34 100644
--- a/website/src/app.tsx
+++ b/website/src/app.tsx
@@ -49,9 +49,7 @@ Track things two ways. Type \`+ \` for a circle checkbox task, or \`[] \` for a
Drop in an image and it renders right where you wrote it. Paste or drag one in to upload your own:
-
-
-Small images flow inline  with the surrounding text.
+Small images flow inline  with the surrounding text.
Paste a YouTube or tweet link and it embeds itself. Undo once to get the plain link back:
From 47e408089a967a366d570e44a9affabecfd6b372 Mon Sep 17 00:00:00 2001
From: ocavue
Date: Sat, 27 Jun 2026 21:57:35 +1000
Subject: [PATCH 4/4] www
---
packages/core/src/extensions/image.ts | 52 +++++++----
packages/core/src/extensions/inline-marks.ts | 32 ++++++-
.../inline-text-to-mark-chunks.test.ts | 50 ++++-------
.../extensions/inline-text-to-mark-chunks.ts | 86 ++++++++++---------
packages/core/src/extensions/mark-names.ts | 2 +
packages/core/src/lezer/inline.test.ts | 14 +++
packages/core/src/style.css | 28 +++++-
7 files changed, 171 insertions(+), 93 deletions(-)
diff --git a/packages/core/src/extensions/image.ts b/packages/core/src/extensions/image.ts
index 3e9f991e..4734ea9f 100644
--- a/packages/core/src/extensions/image.ts
+++ b/packages/core/src/extensions/image.ts
@@ -10,7 +10,7 @@ import { Plugin, PluginKey } from '@prosekit/pm/state'
import type { EditorView, MarkViewConstructor } from '@prosekit/pm/view'
import { listenForTweetHeight, matchEmbed, type EmbedDescriptor } from './embed/index.ts'
-import type { MdImageViewAttrs } from './inline-marks.ts'
+import type { MdImageV2Attrs, MdImageViewAttrs } from './inline-marks.ts'
import type { MarkName } from './mark-names.ts'
type ImageUrlResolver = (src: string) => string | undefined
@@ -54,6 +54,7 @@ function buildEmbedIframe(embed: EmbedDescriptor): HTMLIFrameElement {
function renderImagePreview(
src: string,
alt: string,
+ // TODO: add a title
options: ImageOptions,
): HTMLElement | undefined {
const embed = matchEmbed(src)
@@ -63,16 +64,19 @@ function renderImagePreview(
wrapper.appendChild(buildEmbedIframe(embed))
return wrapper
}
+
const url = (options.resolveImageUrl ?? defaultResolveImageUrl)(src)
if (!url) return undefined
+
const wrapper = document.createElement('span')
- wrapper.className = 'md-image-preview md-image-preview-img'
- wrapper.dataset.testid = 'image-preview'
+ // wrapper.className = 'md-image-preview md-image-preview-img'
+ // wrapper.dataset.testid = 'image-preview'
const img = document.createElement('img')
img.src = url
img.alt = alt
img.draggable = false
wrapper.appendChild(img)
+
return wrapper
}
@@ -84,32 +88,48 @@ function renderImagePreview(
*/
function createImageMarkView(options: ImageOptions): MarkViewConstructor {
return (mark) => {
- const attrs = mark.attrs as MdImageViewAttrs
+ const attrs = mark.attrs as MdImageV2Attrs
+ const { src, alt } = attrs
+
+ const span1 = document.createElement('span')
+ const span2 = document.createElement('span')
+
+ span1.style.width = '20px'
+ span1.style.height = '20px'
+ span1.style.display = 'inline-block'
+ span1.style.margin = '5px'
+ span1.contentEditable = 'false'
+
+ span2.style.width = '20px'
+ span2.style.height = '20px'
+ span2.style.display = 'inline-block'
+ span2.style.margin = '5px'
+
+ span1.style.backgroundColor = 'pink'
+ span2.style.backgroundColor = 'lightblue'
const dom = document.createElement('span')
- dom.className = 'md-image-view'
+ dom.append(span1)
+ dom.className = 'md-image-view-v2'
const contentDOM = document.createElement('span')
- contentDOM.className = 'md-image-view-content'
+ contentDOM.className = 'md-image-view-content-v2'
+ contentDOM.contentEditable = 'true'
+
+ // dom.append(document.createElement('span'))
dom.appendChild(contentDOM)
- const preview = renderImagePreview(attrs.src, attrs.alt, options)
+ const preview = renderImagePreview(src, alt, options)
if (preview) {
preview.contentEditable = 'false'
dom.appendChild(preview)
}
-
- console.log('creating image mark view', attrs.src, attrs.alt, preview)
+ dom.append(span2)
return {
dom,
contentDOM,
ignoreMutation: (mutation) => {
- let result = !contentDOM.contains(mutation.target)
- console.log('ignoreMutation', result, mutation)
- return false
- },
- destroy: () => {
- console.log('destroying image mark view')
+ return !contentDOM.contains(mutation.target)
},
}
}
@@ -179,7 +199,7 @@ function createImageInputPlugin(options: ImageOptions): Plugin {
export function defineImage(options: ImageOptions = {}): PlainExtension {
return union(
defineMarkView({
- name: 'mdImageView' satisfies MarkName,
+ name: 'mdImageV2' satisfies MarkName,
constructor: createImageMarkView(options),
}),
// High priority so the drop/paste handler runs before ProseKit's
diff --git a/packages/core/src/extensions/inline-marks.ts b/packages/core/src/extensions/inline-marks.ts
index 6b9acbd6..263c954c 100644
--- a/packages/core/src/extensions/inline-marks.ts
+++ b/packages/core/src/extensions/inline-marks.ts
@@ -21,6 +21,22 @@ export interface MdImageViewAttrs {
alt: string
}
+export interface MdImageV2Attrs {
+ src: string
+ alt: string
+ title: string
+}
+
+function defineMdImageV2() {
+ return defineMarkSpec<'mdImageV2', MdImageV2Attrs>({
+ name: 'mdImageV2' satisfies MarkName,
+ inclusive: false,
+ attrs: { src: { default: '' }, alt: { default: '' }, title: { default: '' } },
+ toDOM: () => ['span', { class: 'md-image-v2' }, 0],
+ parseDOM: [{ tag: 'span.md-image-v2' }],
+ })
+}
+
/**
* Covers the whole `` source. This is the mark `defineMarkMode`
* hides in hide/focus mode so the rendered image replaces the raw syntax. The
@@ -33,8 +49,8 @@ function defineMdImageSource() {
name: 'mdImageSource' satisfies MarkName,
inclusive: false,
attrs: { src: { default: '' }, alt: { default: '' } },
- toDOM: () => ['span', { class: 'md-image-source' }, 0],
- parseDOM: [{ tag: 'span.md-image-source' }],
+ toDOM: () => ['span', { class: 'md-image-source-v2' }, 0],
+ parseDOM: [{ tag: 'span.md-image-source-v2' }],
})
}
@@ -235,12 +251,23 @@ function defineMdPack() {
})
}
+function defineMdHide() {
+ return defineMarkSpec({
+ name: 'mdHide' satisfies MarkName,
+ inclusive: false,
+ toDOM: () => ['span', { class: 'md-hide' }, 0],
+ parseDOM: [{ tag: 'span.md-hide' }],
+ })
+}
+
export function defineInlineMarks() {
// The last mark registered gets the lowest rank and becomes the outermost DOM
// wrapper, so the wikilink/image marks go last: the view mark (mdWikilinkView /
// mdImageView) wraps the source, which wraps the syntax marks. The pack mark
// goes last of all, so it wraps the whole unit (including a mark view).
return union(
+ defineMdHide(),
+
defineMdMark(),
defineMdEm(),
defineMdStrong(),
@@ -256,6 +283,7 @@ export function defineInlineMarks() {
defineMdWikilinkView(),
defineMdImageSource(),
defineMdImageView(),
+ defineMdImageV2(),
defineMdPack(),
)
}
diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts
index 8541b4c2..89711fbf 100644
--- a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts
+++ b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts
@@ -274,39 +274,25 @@ describe('link', () => {
describe('image', () => {
it('image', () => {
- expect(parse('see  end')).toMatchInlineSnapshot(`
+ expect(parse('')).toMatchInlineSnapshot(`
"
- [0, 4]
- [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) + 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]
+ [0, 19] mdImageV2(src=URL,alt=ALT,title=TITLE) + mdImageSource(src=URL,alt=ALT)
"
`)
})
- it('empty alt', () => {
- expect(parse('')).toMatchInlineSnapshot(`
+ it('only URL', () => {
+ expect(parse('')).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) + mdImageView(src=z.png) + mdLinkUri
- [9, 10] mdPack(key=image,data={"src":"z.png"}) + mdImageSource(src=z.png) + mdMark
+ [0, 14] mdImageV2(src=image.png) + mdImageSource(src=image.png)
"
`)
})
- it('title', () => {
- expect(parse('')).toMatchInlineSnapshot(`
+ it('empty title', () => {
+ expect(parse('')).toMatchInlineSnapshot(`
"
- [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) + 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) + mdMark
+ [0, 17] mdImageV2(src=http://x,alt=a) + mdImageSource(src=http://x,alt=a)
"
`)
})
@@ -314,15 +300,17 @@ describe('image', () => {
it('formatted alt', () => {
expect(parse('')).toMatchInlineSnapshot(`
"
- [0, 2] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdMark
- [2, 4] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c)
- [4, 6] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdStrong + mdMark
- [6, 7] mdPack(key=image,data={"src":"http://x"}) + mdImageSource(src=http://x,alt=a **b** c) + mdStrong
- [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) + 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
+ [0, 22] mdImageV2(src=http://x,alt=a **b** c) + mdImageSource(src=http://x,alt=a **b** c)
+ "
+ `)
+ })
+
+ it('wrapped by text', () => {
+ expect(parse('text  text')).toMatchInlineSnapshot(`
+ "
+ [0, 5]
+ [5, 14] mdImageV2(src=url,alt=a) + mdImageSource(src=url,alt=a)
+ [14, 19]
"
`)
})
diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.ts
index 78e4dfbf..caf2ce37 100644
--- a/packages/core/src/extensions/inline-text-to-mark-chunks.ts
+++ b/packages/core/src/extensions/inline-text-to-mark-chunks.ts
@@ -7,7 +7,7 @@ import { LEZER_NODE_IDS } from '../lezer/node-ids.ts'
import type {
MdImageSourceAttrs,
- MdImageViewAttrs,
+ MdImageV2Attrs,
MdLinkTextAttrs,
MdPackAttrs,
MdPackSimpleKey,
@@ -154,7 +154,7 @@ function walk(
}
/**
- * Special walker for `Link` / `Image` nodes.
+ * Special walker for `Link` nodes.
*
* Lezer's flat child list looks like:
* LinkMark `[` (or `![`), [label children + implicit gaps], LinkMark `]`,
@@ -231,12 +231,6 @@ function walkLink(
/**
* Special walker for a direct image ``.
- *
- * Emits `mdImageSource` across the whole node (the mark `defineMarkMode` hides)
- * and `mdImageView({ src, alt })` across the parsed URL range, where the mark
- * view renders the inline image. `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.
*/
function walkImage(
node: InlineElement,
@@ -252,44 +246,52 @@ function walkImage(
walkLink(node, parentMarks, text, marks, out)
return
}
- const brackets = node.children.filter((child) => child.type === LEZER_NODE_IDS.LinkMark)
- const src = text.slice(urlNode.from, urlNode.to)
- const alt = brackets.length >= 2 ? text.slice(brackets[0].to, brackets[1].from) : ''
- const source = marks.mdImageSource.create({ src, alt } satisfies MdImageSourceAttrs)
- const view = marks.mdImageView.create({ src, alt } satisfies MdImageViewAttrs)
- const pack = marks.mdPack.create({ key: `image`, data: { src } } satisfies MdPackAttrs)
+ const bracketNodes = node.children.filter((child) => child.type === LEZER_NODE_IDS.LinkMark)
+ const titleNode = node.children.find((child) => child.type === LEZER_NODE_IDS.LinkTitle)
- const viewFrom = urlNode.from
- const viewTo = urlNode.to
- const sourceMarks = [...parentMarks, pack, source]
- const viewMarks = [...sourceMarks, view]
+ const src: string = text.slice(urlNode.from, urlNode.to)
+ const alt: string =
+ bracketNodes.length >= 2 ? text.slice(bracketNodes[0].to, bracketNodes[1].from) : ''
+ const title: string = titleNode ? unquoteTitle(text.slice(titleNode.from, titleNode.to)) : ''
- // Marks shared by every chunk at `from`: the `mdPack` envelope plus
- // `mdImageSource` over the whole source, plus `mdImageView` across the URL
- // range. Each child layers its own syntax mark on top.
- const baseAt = (from: number): Mark[] =>
- from >= viewFrom && from < viewTo ? viewMarks : sourceMarks
+ // const source = marks.mdImageSource.create({ src, alt } satisfies MdImageSourceAttrs)
+ // const view = marks.mdImageView.create({ src, alt } satisfies MdImageViewAttrs)
+ // const pack = marks.mdPack.create({ key: `image`, data: { src } } satisfies MdPackAttrs)
- let pos = node.from
- for (const child of node.children) {
- if (child.from > pos) {
- emit(out, pos, child.from, baseAt(pos))
- }
- const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(child.type)
- const childMarks = maybeMarkName
- ? [...baseAt(child.from), marks[maybeMarkName].create()]
- : baseAt(child.from)
- if (child.children.length === 0) {
- emit(out, child.from, child.to, childMarks)
- } else {
- walk(child.children, childMarks, child.from, child.to, text, marks, out)
- }
- pos = child.to
- }
- if (pos < node.to) {
- emit(out, pos, node.to, baseAt(pos))
- }
+ emit(out, node.from, node.to, [
+ ...parentMarks,
+
+ marks.mdImageV2.create({ src, alt, title }),
+ marks.mdImageSource.create({ src, alt }),
+ marks.mdHide.create(),
+ ])
+
+ // // Marks shared by every chunk at `from`: the `mdPack` envelope plus
+ // // `mdImageSource` over the whole source, plus `mdImageView` across the URL
+ // // range. Each child layers its own syntax mark on top.
+ // const baseAt = (from: number): Mark[] =>
+ // from >= viewFrom && from < viewTo ? viewMarks : sourceMarks
+
+ // let pos = node.from
+ // for (const child of node.children) {
+ // if (child.from > pos) {
+ // emit(out, pos, child.from, baseAt(pos))
+ // }
+ // const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(child.type)
+ // const childMarks = maybeMarkName
+ // ? [...baseAt(child.from), marks[maybeMarkName].create()]
+ // : baseAt(child.from)
+ // if (child.children.length === 0) {
+ // emit(out, child.from, child.to, childMarks)
+ // } else {
+ // walk(child.children, childMarks, child.from, child.to, text, marks, out)
+ // }
+ // pos = child.to
+ // }
+ // if (pos < node.to) {
+ // emit(out, pos, node.to, baseAt(pos))
+ // }
}
/**
diff --git a/packages/core/src/extensions/mark-names.ts b/packages/core/src/extensions/mark-names.ts
index 708ef726..aa0f5968 100644
--- a/packages/core/src/extensions/mark-names.ts
+++ b/packages/core/src/extensions/mark-names.ts
@@ -1,4 +1,5 @@
export const MARK_NAMES = [
+ 'mdImageV2',
'mdImageView',
'mdImageSource',
'mdMark',
@@ -14,6 +15,7 @@ export const MARK_NAMES = [
'mdWikilinkSource',
'mdWikilinkView',
'mdPack',
+ 'mdHide',
] as const
export type MarkName = (typeof MARK_NAMES)[number]
diff --git a/packages/core/src/lezer/inline.test.ts b/packages/core/src/lezer/inline.test.ts
index 9294ab32..cc992cdd 100644
--- a/packages/core/src/lezer/inline.test.ts
+++ b/packages/core/src/lezer/inline.test.ts
@@ -96,6 +96,20 @@ describe('image', () => {
`)
})
+ it('can parse image with title', () => {
+ expect(parse('')).toMatchInlineSnapshot(`
+ "
+ Image [0, 19] ""
+ LinkMark [0, 2] "!["
+ LinkMark [5, 6] "]"
+ LinkMark [6, 7] "("
+ URL [7, 10] "url"
+ LinkTitle [11, 18] "\\"title\\""
+ LinkMark [18, 19] ")"
+ "
+ `)
+ })
+
it('can parse image with inline HTML comment', () => {
expect(parse('')).toMatchInlineSnapshot(`
"
diff --git a/packages/core/src/style.css b/packages/core/src/style.css
index fa84be64..6e1a2a8b 100644
--- a/packages/core/src/style.css
+++ b/packages/core/src/style.css
@@ -432,17 +432,18 @@
* decoration, so match the mark view that contains a selected span and ring it
* like a node selection (`.ProseMirror-selectednode`). In show mode the raw
* source is visible and uses the native selection highlight instead. */
-.ProseMirror[data-mark-mode='hide'] .md-image-view:has(.md-atomic-selected) .md-image-preview,
+/*.ProseMirror[data-mark-mode='hide'] .md-image-view:has(.md-atomic-selected) .md-image-preview,*/
.ProseMirror .md-wikilink-view:has(.md-atomic-selected) .md-wikilink-label {
outline: 2px solid var(--meowdown-node-outline);
outline-offset: 2px;
border-radius: 0.25rem;
background: var(--meowdown-node-selection);
}
-/* The image preview keeps its own corner radius. */
+/*/* The image preview keeps its own corner radius. */
.ProseMirror[data-mark-mode='hide'] .md-image-view:has(.md-atomic-selected) .md-image-preview {
border-radius: var(--meowdown-image-radius, 0.5rem);
}
+*/
/* While a whole atomic unit is selected, the selection covers its (often hidden)
* source, so hide the text caret: the ring/native selection is the affordance,
@@ -574,3 +575,26 @@
min-width: 0px;
overflow: visible;
}
+
+.ProseMirror {
+ .md-image-view-v2 img {
+ display: inline-flex;
+ vertical-align: bottom;
+ }
+}
+
+.ProseMirror {
+ .md-hide {
+ overflow: hidden;
+ display: inline-block;
+ vertical-align: bottom;
+
+ letter-spacing: -2em;
+ width: 0px;
+ min-width: 0px;
+ max-width: 0px;
+ max-height: 0px;
+ color: red;
+ visibility: hidden;
+ }
+}