fix: Store src in EmbedSource itself

This commit is contained in:
4513ECHO 2026-05-12 20:11:06 +09:00
parent e41c110c33
commit db0a16edff
No known key found for this signature in database
GPG key ID: 3A5BDFC6E5C365DD
4 changed files with 31 additions and 34 deletions

View file

@ -3,16 +3,16 @@ import type { EditorView, WidgetType } from "@codemirror/view";
export type WidgetInit =
| { state: "resolving"; url: string }
| { state: "resolved"; url: string; src: string }
| { state: "resolved"; url: string }
| { state: "loaded"; url: string }
| { state: "failed"; url: string; error: Error };
const resolvedEffect = StateEffect.define<{ url: string; src: string }>();
const resolvedEffect = StateEffect.define<{ url: string }>();
const failedEffect = StateEffect.define<{ url: string; error: Error }>();
const loadedEffect = StateEffect.define<{ url: string }>();
export function resolved(view: EditorView, url: string, src: string): void {
view.dispatch({ effects: resolvedEffect.of({ url, src }) });
export function resolved(view: EditorView, url: string): void {
view.dispatch({ effects: resolvedEffect.of({ url }) });
}
export function failed(view: EditorView, url: string, error: Error): void {
@ -29,10 +29,7 @@ export function* constructWidget<T extends WidgetType>(
): Generator<[string, T]> {
for (const effect of effects) {
if (effect.is(resolvedEffect)) {
yield [
effect.value.url,
new widget({ state: "resolved", url: effect.value.url, src: effect.value.src }),
];
yield [effect.value.url, new widget({ state: "resolved", url: effect.value.url })];
} else if (effect.is(failedEffect)) {
yield [
effect.value.url,

View file

@ -20,8 +20,8 @@ export abstract class EmbedSource {
constructor(url: string) {
this.url = url;
}
abstract render(src: string): HTMLElement;
abstract resolveSrc(): string | Promise<string>;
abstract render(): HTMLElement;
resolveSrc(): void | Promise<void> {}
get height(): number | undefined {
return undefined;
}

View file

@ -49,6 +49,7 @@ export class Bluesky extends EmbedSource {
static #heightCache: Map<string, number> = new Map();
static id = 0;
#id = (Bluesky.id++).toString();
#src?: string;
constructor(url: string) {
super(url);
}
@ -61,7 +62,7 @@ export class Bluesky extends EmbedSource {
};
}
render(src: string): HTMLElement {
render(): HTMLElement {
const searchParams = new URLSearchParams({
id: this.#id,
colorMode: document.body.classList.contains("theme-dark") ? "dark" : "light",
@ -69,7 +70,7 @@ export class Bluesky extends EmbedSource {
const iframe = createEl("iframe", {
cls: ["external-embed", "node-insert-event"],
attr: {
src: src + "?" + searchParams.toString(),
src: this.#src + "?" + searchParams.toString(),
loading: "lazy",
"data-bluesky-id": this.#id,
},
@ -80,8 +81,13 @@ export class Bluesky extends EmbedSource {
return iframe;
}
override resolveSrc(): string | Promise<string> {
return resolveEmbedSrc(this.url);
override resolveSrc(): void | Promise<void> {
if (this.#src) {
return;
}
return resolveEmbedSrc(this.url).then((src) => {
this.#src = src;
});
}
override get height(): number | undefined {

View file

@ -30,7 +30,6 @@ function Container(url: string, state: WidgetInit["state"]): HTMLElement {
export class EmbedWidget extends WidgetType {
#url: string;
#state: WidgetInit["state"];
#src?: string;
#error?: Error;
#embedSource: EmbedSource;
constructor(init: WidgetInit) {
@ -38,9 +37,6 @@ export class EmbedWidget extends WidgetType {
this.#url = init.url;
this.#state = init.state;
switch (init.state) {
case "resolved":
this.#src = init.src;
break;
case "failed":
this.#error = init.error;
break;
@ -58,24 +54,23 @@ export class EmbedWidget extends WidgetType {
switch (this.#state) {
case "resolving":
container.appendChild(Loading(this.#embedSource.height));
const srcOrPromise = this.#embedSource.resolveSrc();
if (srcOrPromise instanceof Promise) {
srcOrPromise
.then((src) => resolved(view, this.#url, src))
const needResolve = this.#embedSource.resolveSrc();
if (needResolve instanceof Promise) {
needResolve
.then(() => resolved(view, this.#url))
.catch((error) => failed(view, this.#url, error));
} else {
this.#state = "resolved";
this.#src = srcOrPromise;
container.setAttribute("data-state", "resolved");
container.appendChild(this.#embedSource.render(this.#src!));
container.appendChild(this.#embedSource.render());
}
break;
case "resolved":
container.appendChild(Loading(this.#embedSource.height));
container.appendChild(this.#embedSource.render(this.#src!));
container.appendChild(this.#embedSource.render());
break;
case "loaded":
container.appendChild(this.#embedSource.render(this.#src!));
container.appendChild(this.#embedSource.render());
break;
case "failed":
container.appendChild(ErrorMessage(this.#error!));
@ -102,7 +97,7 @@ export class EmbedWidget extends WidgetType {
case "resolving":
return false;
case "resolved":
dom.appendChild(this.#embedSource.render(this.#src!));
dom.appendChild(this.#embedSource.render());
return true;
case "loaded":
const iframe = dom.querySelector("iframe");
@ -119,7 +114,6 @@ export class EmbedWidget extends WidgetType {
}
}
// TODO: use "loading" state
export function createElement(url: string, dom: HTMLElement): void {
const embedSource = EmbedSourceRegistry.lookup(url);
if (!embedSource) {
@ -136,12 +130,12 @@ export function createElement(url: string, dom: HTMLElement): void {
}
loading.remove();
});
const srcOrPromise = embedSource.resolveSrc();
if (srcOrPromise instanceof Promise) {
srcOrPromise
.then((src) => {
const needResolve = embedSource.resolveSrc();
if (needResolve instanceof Promise) {
needResolve
.then(() => {
container.setAttribute("data-state", "resolved");
container.appendChild(embedSource.render(src));
container.appendChild(embedSource.render());
})
.catch((error) => {
container.setAttribute("data-state", "failed");
@ -149,7 +143,7 @@ export function createElement(url: string, dom: HTMLElement): void {
loading.remove();
});
} else {
container.appendChild(embedSource.render(srcOrPromise));
container.appendChild(embedSource.render());
}
dom.replaceWith(container);
}