diff --git a/app/assets/javascripts/discourse/app/static/markdown-it/index.js b/app/assets/javascripts/discourse/app/static/markdown-it/index.js
index c460fcd09e3..127d23b7a20 100644
--- a/app/assets/javascripts/discourse/app/static/markdown-it/index.js
+++ b/app/assets/javascripts/discourse/app/static/markdown-it/index.js
@@ -4,7 +4,7 @@ import loadPluginFeatures from "./features";
import MentionsParser from "./mentions-parser";
import buildOptions from "./options";
-function buildEngine(options) {
+export function buildEngine(options) {
return DiscourseMarkdownIt.withCustomFeatures(
loadPluginFeatures()
).withOptions(buildOptions(options));
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/components/prosemirror-editor.gjs b/app/assets/javascripts/discourse/app/static/prosemirror/components/prosemirror-editor.gjs
index 5f8810fa254..0686fbab5cd 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/components/prosemirror-editor.gjs
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/components/prosemirror-editor.gjs
@@ -13,12 +13,14 @@ import {
} from "discourse/lib/composer/rich-editor-extensions";
import * as ProsemirrorModel from "prosemirror-model";
import * as ProsemirrorView from "prosemirror-view";
+import * as ProsemirrorState from "prosemirror-state";
+import * as ProsemirrorHistory from "prosemirror-history";
+import * as ProsemirrorTransform from "prosemirror-transform";
import { createHighlight } from "../plugins/code-highlight";
import { baseKeymap } from "prosemirror-commands";
import { dropCursor } from "prosemirror-dropcursor";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-import * as ProsemirrorState from "prosemirror-state";
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { bind } from "discourse-common/utils/decorators";
@@ -29,12 +31,16 @@ import { convertToMarkdown } from "../lib/serializer";
import { buildInputRules } from "../plugins/inputrules";
import { buildKeymap } from "../plugins/keymap";
import placeholder from "../plugins/placeholder";
+import { gapCursor } from "prosemirror-gapcursor";
export default class ProsemirrorEditor extends Component {
@service appEvents;
@service menu;
@service siteSettings;
+ @service dialog;
+
@tracked rootElement;
+
editorContainerId = guidFor(this);
schema = createSchema();
view;
@@ -59,18 +65,11 @@ export default class ProsemirrorEditor extends Component {
keymap(buildKeymap(this.schema, keymapFromArgs)),
keymap(baseKeymap),
dropCursor({ color: "var(--primary)" }),
+ gapCursor(),
history(),
placeholder(this.args.placeholder),
createHighlight(),
- ...getPlugins().map((plugin) =>
- typeof plugin === "function"
- ? plugin({
- ...ProsemirrorState,
- ...ProsemirrorModel,
- ...ProsemirrorView,
- })
- : new Plugin(plugin)
- ),
+ ...getPlugins().flatMap(processPlugin),
];
this.state = EditorState.create({
@@ -126,9 +125,14 @@ export default class ProsemirrorEditor extends Component {
@bind
convertFromValue() {
- const doc = convertFromMarkdown(this.schema, this.args.value);
-
- // console.log("Resulting doc:", doc);
+ let doc;
+ try {
+ doc = convertFromMarkdown(this.schema, this.args.value);
+ } catch (e) {
+ console.error(e);
+ this.dialog.alert(e.message);
+ return;
+ }
const tr = this.state.tr
.replaceWith(0, this.state.doc.content.size, doc.content)
@@ -151,3 +155,19 @@ export default class ProsemirrorEditor extends Component {
}
+
+function processPlugin(plugin) {
+ if (typeof plugin === "function") {
+ return plugin({
+ ...ProsemirrorState,
+ ...ProsemirrorModel,
+ ...ProsemirrorView,
+ ...ProsemirrorHistory,
+ ...ProsemirrorTransform,
+ });
+ }
+ if (plugin instanceof Array) {
+ return plugin.map(processPlugin);
+ }
+ return new Plugin(plugin);
+}
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/code-lang-selector.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/code-lang-selector.js
index 899feb5e623..550fbc2a650 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/code-lang-selector.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/code-lang-selector.js
@@ -33,8 +33,6 @@ class CodeBlockWithLangSelectorNodeView {
this.dom.appendChild(select);
- // TODO(renato): leaving with the keyboard to before the node doesn't work
-
const code = document.createElement("code");
this.dom.appendChild(document.createElement("pre")).appendChild(code);
this.contentDOM = code;
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/emoji.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/emoji.js
index 14b04ef7a1c..56768d608fb 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/emoji.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/emoji.js
@@ -1,7 +1,9 @@
import { buildEmojiUrl, emojiExists, isCustomEmoji } from "pretty-text/emoji";
+import { translations } from "pretty-text/emoji/data";
import { emojiOptions } from "discourse/lib/text";
+import { isBoundary } from "discourse/static/prosemirror/lib/markdown-it";
+import escapeRegExp from "discourse-common/utils/escape-regexp";
-// TODO(renato): we need to avoid the invalid text:emoji: state (reminder to use isPunctChar to avoid deleting the space)
export default {
nodeSpec: {
emoji: {
@@ -52,6 +54,18 @@ export default {
},
options: { undoable: false },
},
+ {
+ match: new RegExp(
+ `(?<=^|\\W)(${Object.keys(translations).map(escapeRegExp).join("|")})$`
+ ),
+ handler: (state, match, start, end) => {
+ return state.tr.replaceWith(
+ start,
+ end,
+ state.schema.nodes.emoji.create({ code: translations[match[1]] })
+ );
+ },
+ },
],
parse: {
@@ -64,7 +78,11 @@ export default {
},
serializeNode: {
- emoji: (state, node) => {
+ emoji(state, node) {
+ if (!isBoundary(state.out, state.out.length - 1)) {
+ state.write(" ");
+ }
+
state.write(`:${node.attrs.code}:`);
},
},
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/hashtag.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/hashtag.js
index 6e4e037a440..388cda38479 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/hashtag.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/hashtag.js
@@ -1,3 +1,5 @@
+import { isBoundary } from "discourse/static/prosemirror/lib/markdown-it";
+
export default {
nodeSpec: {
hashtag: {
@@ -30,7 +32,7 @@ export default {
inputRules: [
{
- match: /(?<=^|\W)#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101}) $/,
+ match: /(?<=^|\W)#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})\s$/,
handler: (state, match, start, end) =>
state.selection.$from.nodeBefore?.type !== state.schema.nodes.hashtag &&
state.tr.replaceWith(start, end, [
@@ -42,7 +44,7 @@ export default {
],
parse: {
- span: (state, token, tokens, i) => {
+ span(state, token, tokens, i) {
if (token.attrGet("class") === "hashtag-raw") {
state.openNode(state.schema.nodes.hashtag, {
name: tokens[i + 1].content.slice(1),
@@ -53,8 +55,18 @@ export default {
},
serializeNode: {
- hashtag: (state, node) => {
+ hashtag(state, node, parent, index) {
+ if (!isBoundary(state.out, state.out.length - 1)) {
+ state.write(" ");
+ }
+
state.write(`#${node.attrs.name}`);
+
+ const nextSibling =
+ parent.childCount > index + 1 ? parent.child(index + 1) : null;
+ if (nextSibling?.isText && !isBoundary(nextSibling.text, 0)) {
+ state.write(" ");
+ }
},
},
};
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js
index 2abf7487ae7..c2c6a17d955 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js
@@ -1,6 +1,47 @@
export default {
nodeSpec: {
- // TODO(renato): html_block should be like a passthrough code block
- html_block: { block: "paragraph", noCloseToken: true },
+ html_block: {
+ attrs: {
+ content: { default: "" },
+ },
+ group: "block",
+ content: "block*",
+ // it's too broad to be automatically parsed
+ parseDOM: [],
+ toDOM: (node) => {
+ const dom = document.createElement("template");
+ dom.innerHTML = node.attrs.content;
+ return dom.content.firstChild;
+ },
+ },
+ },
+ parse: {
+ // TODO(renato): should html_block be like a passthrough code block?
+ html_block: (state, token) => {
+ const openMatch = token.content.match(
+ /^<([a-zA-Z][a-zA-Z0-9-]*)(?:\s[^>]*)?>.*/
+ );
+ const closeMatch = token.content.match(
+ /^<\/([a-zA-Z][a-zA-Z0-9-]*)>\s*$/
+ );
+
+ if (openMatch) {
+ state.openNode(state.schema.nodes.html_block, {
+ content: openMatch[0],
+ });
+
+ return;
+ }
+
+ if (closeMatch) {
+ state.closeNode();
+ }
+ },
+ },
+ serializeNode: {
+ html_block: (state, node) => {
+ state.write(node.attrs.content);
+ state.renderContent(node);
+ },
},
};
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-inline.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-inline.js
index a3b00a9a3ff..34e8214fb23 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-inline.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-inline.js
@@ -28,6 +28,7 @@ export default {
html_inline: {
group: "inline",
inline: true,
+ isolating: true,
content: "inline*",
attrs: { tag: {} },
parseDOM: ALLOWED_INLINE.map((tag) => ({ tag })),
@@ -37,8 +38,8 @@ export default {
parse: {
// TODO(renato): it breaks if it's missing an end tag
html_inline: (state, token) => {
- const openMatch = token.content.match(/^<([a-z]+)>$/u);
- const closeMatch = token.content.match(/^<\/([a-z]+)>$/u);
+ const openMatch = token.content.match(/^<([a-z]+)>$/);
+ const closeMatch = token.content.match(/^<\/([a-z]+)>$/);
if (openMatch) {
const tagName = openMatch[1];
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/index.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/index.js
index e8e423be8a0..eca9fb5391c 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/index.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/index.js
@@ -18,8 +18,8 @@ import typographerReplacements from "./typographer-replacements";
import underline from "./underline";
const defaultExtensions = [
+ // emoji before image
emoji,
- // image must be after emoji
image,
hashtag,
mention,
@@ -27,16 +27,17 @@ const defaultExtensions = [
underline,
htmlInline,
htmlBlock,
+ // onebox before link
+ onebox,
link,
heading,
codeBlock,
quote,
- onebox,
trailingParagraph,
typographerReplacements,
markdownPaste,
- // table must be last
+ // table last
table,
];
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/link.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/link.js
index 49e05c77bd6..b4df4805d2c 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/link.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/link.js
@@ -1,25 +1,114 @@
+import { getLinkify } from "../lib/markdown-it";
+
+const markdownUrlInputRule = ({ schema, markInputRule }) =>
+ markInputRule(
+ /\[([^\]]+)]\(([^)\s]+)(?:\s+[“"']([^“"']+)[”"'])?\)$/,
+ schema.marks.link,
+ (match) => {
+ return { href: match[2], title: match[3] };
+ }
+ );
+
export default {
- inputRules: [
- // []() replacement
- ({ schema, markInputRule }) =>
- markInputRule(
- /\[([^\]]+)]\(([^)\s]+)(?:\s+[“"']([^“"']+)[”"'])?\)$/,
- schema.marks.link,
- (match) => {
- return { href: match[2], title: match[3] };
- }
- ),
- // TODO(renato): auto-linkify when typing (https://github.com/markdown-it/markdown-it/blob/master/lib/rules_inline/autolink.mjs)
- ],
- plugins: ({ Plugin, Slice, Fragment }) =>
+ markSpec: {
+ link: {
+ attrs: {
+ href: {},
+ title: { default: null },
+ autoLink: { default: null },
+ },
+ inclusive: false,
+ parseDOM: [
+ {
+ tag: "a[href]",
+ getAttrs(dom) {
+ return {
+ href: dom.getAttribute("href"),
+ title: dom.getAttribute("title"),
+ };
+ },
+ },
+ ],
+ toDOM(node) {
+ return ["a", { href: node.attrs.href, title: node.attrs.title }];
+ },
+ },
+ },
+ parse: {
+ link: {
+ mark: "link",
+ getAttrs: (tok) => ({
+ href: tok.attrGet("href"),
+ title: tok.attrGet("title") || null,
+ autoLink: tok.markup === "autolink",
+ }),
+ },
+ },
+ inputRules: [markdownUrlInputRule],
+ plugins: ({
+ Plugin,
+ Slice,
+ Fragment,
+ undoDepth,
+ ReplaceAroundStep,
+ ReplaceStep,
+ AddMarkStep,
+ RemoveMarkStep,
+ }) =>
new Plugin({
+ // Auto-linkify typed URLs
+ appendTransaction: (transactions, prevState, state) => {
+ const isUndo = undoDepth(prevState) - undoDepth(state) === 1;
+ if (isUndo) {
+ return;
+ }
+
+ const docChanged = transactions.some(
+ (transaction) => transaction.docChanged
+ );
+ if (!docChanged) {
+ return;
+ }
+
+ const composedTransaction = composeSteps(transactions, prevState);
+ const changes = getChangedRanges(
+ composedTransaction,
+ [ReplaceAroundStep, ReplaceStep],
+ [AddMarkStep, ReplaceAroundStep, ReplaceStep, RemoveMarkStep]
+ );
+ const { mapping } = composedTransaction;
+ const { tr, doc } = state;
+
+ for (const { prevFrom, prevTo, from, to } of changes) {
+ findTextBlocksInRange(doc, { from, to }).forEach(
+ ({ text, positionStart }) => {
+ const matches = getLinkify().match(text);
+ if (!matches) {
+ return;
+ }
+
+ for (const match of matches) {
+ const { index, lastIndex, raw } = match;
+ const start = positionStart + index;
+ const end = positionStart + lastIndex + 1;
+ const href = raw;
+ // TODO not ready yet
+ // tr.setMeta("autolinking", true).addMark(
+ // start,
+ // end,
+ // state.schema.marks.link.create({ href })
+ // );
+ }
+ }
+ );
+ }
+
+ return tr;
+ },
props: {
// Auto-linkify plain-text pasted URLs
- // TODO(renato): URLs copied from HTML will go through the regular HTML parsing
- // it would be nice to auto-linkify them too
clipboardTextParser(text, $context, plain, view) {
- // TODO(renato): a less naive regex, reuse existing
- if (!text.match(/^https?:\/\//) || view.state.selection.empty) {
+ if (view.state.selection.empty || !getLinkify().test(text)) {
return;
}
@@ -34,6 +123,119 @@ export default {
]);
return new Slice(Fragment.from(textNode), 0, 0);
},
+
+ // Auto-linkify rich content with a single text node that is a URL
+ transformPasted(paste, view) {
+ if (
+ paste.content.childCount === 1 &&
+ paste.content.firstChild.isText &&
+ !paste.content.firstChild.marks.some(
+ (mark) => mark.type.name === "link"
+ )
+ ) {
+ const matches = linkify.match(paste.content.firstChild.text);
+ const isFullMatch =
+ matches &&
+ matches.length === 1 &&
+ matches[0].raw === paste.content.firstChild.text;
+
+ if (!isFullMatch) {
+ return paste;
+ }
+
+ const marks = view.state.selection.$head.marks();
+ const originalText = view.state.doc.textBetween(
+ view.state.selection.from,
+ view.state.selection.to
+ );
+ const textNode = view.state.schema.text(originalText, [
+ ...marks,
+ view.state.schema.marks.link.create({
+ href: paste.content.firstChild.text,
+ }),
+ ]);
+ paste = new Slice(Fragment.from(textNode), 0, 0);
+ }
+ return paste;
+ },
},
}),
};
+
+function composeSteps(transactions, prevState) {
+ const { tr } = prevState;
+
+ transactions.forEach((transaction) => {
+ transaction.steps.forEach((step) => {
+ tr.step(step);
+ });
+ });
+
+ return tr;
+}
+
+function getChangedRanges(tr, replaceTypes, rangeTypes) {
+ const ranges = [];
+ const { steps, mapping } = tr;
+ const inverseMapping = mapping.invert();
+
+ steps.forEach((step, i) => {
+ if (!isValidStep(step, replaceTypes)) {
+ return;
+ }
+
+ const rawRanges = [];
+ const stepMap = step.getMap();
+ const mappingSlice = mapping.slice(i);
+
+ if (stepMap.ranges.length === 0 && isValidStep(step, rangeTypes)) {
+ const { from, to } = step;
+ rawRanges.push({ from, to });
+ } else {
+ stepMap.forEach((from, to) => {
+ rawRanges.push({ from, to });
+ });
+ }
+
+ rawRanges.forEach((range) => {
+ const from = mappingSlice.map(range.from, -1);
+ const to = mappingSlice.map(range.to);
+
+ ranges.push({
+ from,
+ to,
+ prevFrom: inverseMapping.map(from, -1),
+ prevTo: inverseMapping.map(to),
+ });
+ });
+ });
+
+ return ranges.sort((a, z) => a.from - z.from);
+}
+
+function isValidStep(step, types) {
+ return types.some((type) => step instanceof type);
+}
+
+function findTextBlocksInRange(doc, range) {
+ const nodesWithPos = [];
+
+ // define a placeholder for leaf nodes to calculate link position
+ doc.nodesBetween(range.from, range.to, (node, pos) => {
+ if (!node.isTextblock || !node.type.allowsMarkType("link")) {
+ return;
+ }
+
+ nodesWithPos.push({ node, pos });
+ });
+
+ return nodesWithPos.map((textBlock) => ({
+ text: doc.textBetween(
+ textBlock.pos,
+ textBlock.pos + textBlock.node.nodeSize,
+ undefined,
+ " "
+ ),
+ positionStart: textBlock.pos,
+ }));
+}
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/mention.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/mention.js
index 95d68a4d8e6..2ce1782a01b 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/mention.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/mention.js
@@ -1,5 +1,3 @@
-// TODO(renato): similar to emoji, avoid joining anything@mentions, as it's invalid markdown
-
import { mentionRegex } from "pretty-text/mentions";
export default {
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/onebox.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/onebox.js
index 479c075f0cc..a6ab89bade9 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/onebox.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/onebox.js
@@ -1,13 +1,19 @@
-import { cachedInlineOnebox } from "pretty-text/inline-oneboxer";
+import {
+ applyCachedInlineOnebox,
+ cachedInlineOnebox,
+} from "pretty-text/inline-oneboxer";
+import { addToLoadingQueue, loadNext } from "pretty-text/oneboxer";
import { lookupCache } from "pretty-text/oneboxer-cache";
+import { ajax } from "discourse/lib/ajax";
+import { isBoundary } from "discourse/static/prosemirror/lib/markdown-it";
+import escapeRegExp from "discourse-common/utils/escape-regexp";
export default {
nodeSpec: {
onebox: {
attrs: { url: {}, html: {} },
- selectable: false,
- group: "inline",
- inline: true,
+ selectable: true,
+ group: "block",
atom: true,
draggable: true,
parseDOM: [
@@ -19,18 +25,58 @@ export default {
},
],
toDOM(node) {
- // const dom = document.createElement("aside");
- // dom.outerHTML = node.attrs.html;
-
- // TODO(renato): revisit?
- return new DOMParser().parseFromString(node.attrs.html, "text/html")
- .body.firstChild;
+ const dom = document.createElement("div");
+ dom.classList.add("onebox-wrapper");
+ dom.innerHTML = node.attrs.html;
+ return dom;
+ },
+ },
+ onebox_inline: {
+ attrs: { url: {}, title: {} },
+ inline: true,
+ group: "inline",
+ selectable: true,
+ atom: true,
+ draggable: true,
+ parseDOM: [
+ {
+ // TODO link marks are still processed before this when pasting
+ tag: "a.inline-onebox",
+ getAttrs(dom) {
+ return { url: dom.getAttribute("href"), title: dom.textContent };
+ },
+ },
+ ],
+ toDOM(node) {
+ return [
+ "a",
+ {
+ class: "inline-onebox",
+ href: node.attrs.url,
+ contentEditable: false,
+ },
+ node.attrs.title,
+ ];
},
},
},
serializeNode: {
onebox(state, node) {
- state.write(node.attrs.url);
+ state.ensureNewLine();
+ state.write(`${node.attrs.url}\n\n`);
+ },
+ onebox_inline(state, node, parent, index) {
+ if (!isBoundary(state.out, state.out.length - 1)) {
+ state.write(" ");
+ }
+
+ state.text(node.attrs.url);
+
+ const nextSibling =
+ parent.childCount > index + 1 ? parent.child(index + 1) : null;
+ if (nextSibling?.isText && !isBoundary(nextSibling.text, 0)) {
+ state.write(" ");
+ }
},
},
@@ -38,37 +84,102 @@ export default {
const plugin = new Plugin({
state: {
init() {
- return [];
+ return { full: {}, inline: {} };
},
apply(tr, value) {
- // TODO(renato)
- return value;
+ const updated = { full: [], inline: [] };
+
+ // we shouldn't check all descendants, but only the ones that have changed
+ // it's a problem in other plugins too where we need to optimize
+ tr.doc.descendants((node, pos) => {
+ // if node has the link mark
+ const link = node.marks.find((mark) => mark.type.name === "link");
+ if (
+ !tr.getMeta("autolinking") &&
+ !link?.attrs.autoLink &&
+ link?.attrs.href === node.textContent
+ ) {
+ const resolvedPos = tr.doc.resolve(pos);
+
+ const isAtRoot = resolvedPos.depth === 1;
+
+ const parent = resolvedPos.parent;
+ const index = resolvedPos.index();
+ const prev = index > 0 ? parent.child(index - 1) : null;
+ const next =
+ index < parent.childCount - 1 ? parent.child(index + 1) : null;
+
+ const isAlone =
+ (!prev || prev.type.name === "hard_break") &&
+ (!next || next.type.name === "hard_break");
+
+ const isInline = !isAtRoot || !isAlone;
+
+ const obj = isInline ? updated.inline : updated.full;
+
+ obj[node.textContent] ??= [];
+ obj[node.textContent].push(pos);
+ }
+ });
+
+ return updated;
},
},
view() {
return {
- update(view, prevState) {
+ async update(view, prevState) {
if (prevState.doc.eq(view.state.doc)) {
return;
}
- // console.log("discourse", view.props.discourse);
+ const { full, inline } = plugin.getState(view.state);
- const unresolvedLinks = plugin.getState(view.state);
+ for (const [url, list] of Object.entries(full)) {
+ const html = await loadFullOnebox(url, view.props.discourse);
- // console.log(unresolvedLinks);
+ // naive check that this is not a url onebox response
+ if (
+ new RegExp(
+ `${escapeRegExp(
+ url
+ )}`
+ ).test(html)
+ ) {
+ continue;
+ }
- for (const unresolved of unresolvedLinks) {
- const isInline = unresolved.isInline;
- // console.log(isInline, cachedInlineOnebox(unresolved.text));
+ const tr = view.state.tr;
+ for (const pos of list) {
+ const node = tr.doc.nodeAt(pos);
+ tr.replaceWith(
+ pos - 1,
+ pos + node.nodeSize,
+ view.state.schema.nodes.onebox.create({ url, html })
+ );
+ }
+ tr.setMeta("addToHistory", false);
+ view.dispatch(tr);
+ }
- const className = isInline
- ? "onebox-loading"
- : "inline-onebox-loading";
+ const inlineOneboxes = await loadInlineOneboxes(
+ Object.keys(inline),
+ view.props.discourse
+ );
- if (!isInline) {
- // console.log(lookupCache(unresolved.text));
+ for (const [url, onebox] of Object.entries(inlineOneboxes)) {
+ for (const pos of inline[url]) {
+ const tr = view.state.tr;
+ tr.replaceWith(
+ pos,
+ pos + tr.doc.nodeAt(pos).nodeSize,
+ view.state.schema.nodes.onebox_inline.create({
+ url,
+ title: onebox.title,
+ })
+ );
+ tr.setMeta("addToHistory", false);
+ view.dispatch(tr);
}
}
},
@@ -80,18 +191,45 @@ export default {
},
};
-function isValidUrl(text) {
- try {
- new URL(text); // If it can be parsed as a URL, it's valid.
- return true;
- } catch {
- return false;
+async function loadInlineOneboxes(urls, { categoryId, topicId }) {
+ const allOneboxes = {};
+
+ const uncachedUrls = [];
+ for (const url of urls) {
+ const cached = cachedInlineOnebox(url);
+ if (cached) {
+ allOneboxes[url] = cached;
+ } else {
+ uncachedUrls.push(url);
+ }
}
+
+ if (uncachedUrls.length === 0) {
+ return allOneboxes;
+ }
+
+ const { "inline-oneboxes": oneboxes } = await ajax("/inline-onebox", {
+ data: { urls: uncachedUrls, categoryId, topicId },
+ });
+
+ oneboxes.forEach((onebox) => {
+ if (onebox.title) {
+ applyCachedInlineOnebox(onebox.url, onebox);
+ allOneboxes[onebox.url] = onebox;
+ }
+ });
+
+ return allOneboxes;
}
-function isNodeInline(state, pos) {
- const resolvedPos = state.doc.resolve(pos);
- const parent = resolvedPos.parent;
+async function loadFullOnebox(url, { categoryId, topicId }) {
+ const cached = lookupCache(url);
+ if (cached) {
+ return cached;
+ }
- return parent.childCount !== 1;
+ return new Promise((onResolve) => {
+ addToLoadingQueue({ url, categoryId, topicId, onResolve });
+ loadNext(ajax);
+ });
}
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/table.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/table.js
index fa27ac099ec..00008535cde 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/table.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/table.js
@@ -17,6 +17,8 @@ export default {
group: "block",
tableRole: "table",
isolating: true,
+ selectable: true,
+ draggable: true,
parseDOM: [{ tag: "table" }],
toDOM() {
return ["table", 0];
@@ -124,10 +126,13 @@ export default {
table(state, node) {
state.flushClose(1);
- let headerBuffer = state.delim && state.atBlank() ? state.delim : "";
+ let headerBuffer = state.delim;
const prevInTable = state.inTable;
state.inTable = true;
+ // leading newline, it seems to have issues in a line just below a > blockquote otherwise
+ state.out += "\n";
+
// group is table_head or table_body
node.forEach((group, groupOffset, groupIndex) => {
group.forEach((row) => {
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/trailing-paragraph.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/trailing-paragraph.js
index e6f7bca56b8..48c8789fe99 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/trailing-paragraph.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/trailing-paragraph.js
@@ -16,16 +16,27 @@ export default {
},
state: {
init(_, state) {
- return state.doc.lastChild.type !== state.schema.nodes.paragraph;
+ return !isLastChildEmptyParagraph(state);
},
apply(tr, value) {
if (!tr.docChanged) {
return value;
}
- return tr.doc.lastChild.type !== tr.doc.type.schema.nodes.paragraph;
+ return !isLastChildEmptyParagraph(tr);
},
},
});
},
};
+
+function isLastChildEmptyParagraph(state) {
+ const { doc } = state;
+ const lastChild = doc.lastChild;
+
+ return (
+ lastChild.type.name === "paragraph" &&
+ lastChild.nodeSize === 2 &&
+ lastChild.content.size === 0
+ );
+}
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/lib/markdown-it.js b/app/assets/javascripts/discourse/app/static/prosemirror/lib/markdown-it.js
new file mode 100644
index 00000000000..ea638555756
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/lib/markdown-it.js
@@ -0,0 +1,24 @@
+import { buildEngine } from "discourse/static/markdown-it";
+import loadPluginFeatures from "discourse/static/markdown-it/features";
+import defaultFeatures from "discourse-markdown-it/features/index";
+
+let engine;
+
+function getEngine() {
+ engine ??= buildEngine({
+ featuresOverride: [...defaultFeatures, ...loadPluginFeatures()]
+ .map(({ id }) => id)
+ // Avoid oneboxing when parsing, we'll handle that separately
+ .filter((id) => id !== "onebox"),
+ });
+
+ return engine;
+}
+
+export const parse = (text) => getEngine().parse(text);
+
+export const getLinkify = () => getEngine().linkify;
+
+export const isBoundary = (str, index) =>
+ getEngine().options.engine.utils.isWhiteSpace(str.charCodeAt(index)) ||
+ getEngine().options.engine.utils.isMdAsciiPunct(str.charCodeAt(index));
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/lib/parser.js b/app/assets/javascripts/discourse/app/static/prosemirror/lib/parser.js
index 3261c81721e..25dd4c60e5d 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/lib/parser.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/lib/parser.js
@@ -1,8 +1,6 @@
import { defaultMarkdownParser, MarkdownParser } from "prosemirror-markdown";
import { getParsers } from "discourse/lib/composer/rich-editor-extensions";
-import { parse as markdownItParse } from "discourse/static/markdown-it";
-import loadPluginFeatures from "discourse/static/markdown-it/features";
-import defaultFeatures from "discourse-markdown-it/features/index";
+import { parse } from "./markdown-it";
// TODO(renato): We need a workaround for this parsing issue:
// https://github.com/ProseMirror/prosemirror-markdown/issues/82
@@ -19,9 +17,9 @@ const postParseTokens = {
softbreak: (state) => state.addNode(state.schema.nodes.hard_break),
};
-let parseOptions;
-function initializeParser() {
- if (parseOptions) {
+let initialized;
+function loadCustomParsers() {
+ if (initialized) {
return;
}
@@ -33,18 +31,13 @@ function initializeParser() {
}
}
- const featuresOverride = [...defaultFeatures, ...loadPluginFeatures()]
- .map(({ id }) => id)
- // Avoid oneboxing when parsing, we'll handle that separately
- .filter((id) => id !== "onebox");
-
- parseOptions = { featuresOverride };
+ initialized = true;
}
export function convertFromMarkdown(schema, text) {
- initializeParser();
+ loadCustomParsers();
- const tokens = markdownItParse(text, parseOptions);
+ const tokens = parse(text);
console.log("Converting tokens", tokens);
diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/plugins/keymap.js b/app/assets/javascripts/discourse/app/static/prosemirror/plugins/keymap.js
index bc9a0d17d13..8887bb80b9e 100644
--- a/app/assets/javascripts/discourse/app/static/prosemirror/plugins/keymap.js
+++ b/app/assets/javascripts/discourse/app/static/prosemirror/plugins/keymap.js
@@ -1,8 +1,6 @@
import {
chainCommands,
exitCode,
- joinDown,
- joinUp,
lift,
selectParentNode,
setBlockType,
@@ -48,8 +46,6 @@ export function buildKeymap(schema, initialKeymap = {}, suppressKeys) {
bind("Mod-y", redo);
}
- bind("Alt-ArrowUp", joinUp);
- bind("Alt-ArrowDown", joinDown);
bind("Mod-BracketLeft", lift);
bind("Escape", selectParentNode);
diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json
index bd29f72fe02..37c54076c05 100644
--- a/app/assets/javascripts/discourse/package.json
+++ b/app/assets/javascripts/discourse/package.json
@@ -36,6 +36,7 @@
"pretty-text": "workspace:1.0.0",
"prosemirror-commands": "^1.6.0",
"prosemirror-dropcursor": "^1.8.1",
+ "prosemirror-gapcursor": "^1.3.2",
"prosemirror-highlight": "^0.11.0",
"prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.4.0",
@@ -44,6 +45,7 @@
"prosemirror-model": "^1.23.0",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-state": "^1.4.3",
+ "prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.34.3"
},
"devDependencies": {
diff --git a/app/assets/javascripts/pretty-text/addon/oneboxer.js b/app/assets/javascripts/pretty-text/addon/oneboxer.js
index ba4ffbce0c3..66274fa30a6 100644
--- a/app/assets/javascripts/pretty-text/addon/oneboxer.js
+++ b/app/assets/javascripts/pretty-text/addon/oneboxer.js
@@ -54,7 +54,7 @@ function _handleLoadingOneboxImages() {
this.removeEventListener("load", _handleLoadingOneboxImages);
}
-function loadNext(ajax) {
+export function loadNext(ajax) {
if (loadingQueue.length === 0) {
timeout = null;
return;
@@ -62,7 +62,8 @@ function loadNext(ajax) {
let timeoutMs = 150;
let removeLoading = true;
- const { url, refresh, elem, categoryId, topicId } = loadingQueue.shift();
+ const { url, refresh, elem, categoryId, topicId, onResolve } =
+ loadingQueue.shift();
// Retrieve the onebox
return ajax("/onebox", {
@@ -78,6 +79,7 @@ function loadNext(ajax) {
(template) => {
const node = domFromString(template)[0];
setLocalCache(normalize(url), node);
+ onResolve?.(template);
elem.replaceWith(node);
applySquareGenericOnebox(node);
},
@@ -155,3 +157,17 @@ export function load({
timeout = timeout || discourseLater(() => loadNext(ajax), 150);
}
}
+
+export function addToLoadingQueue({
+ url,
+ elem = {
+ replaceWith() {},
+ classList: { remove() {}, add() {} },
+ dataset: {},
+ },
+ categoryId,
+ topicId,
+ onResolve,
+}) {
+ loadingQueue.push({ url, elem, categoryId, topicId, onResolve });
+}
diff --git a/app/assets/stylesheets/common/rich-editor/rich-editor.scss b/app/assets/stylesheets/common/rich-editor/rich-editor.scss
index 531c1251f67..eb5257b2fdf 100644
--- a/app/assets/stylesheets/common/rich-editor/rich-editor.scss
+++ b/app/assets/stylesheets/common/rich-editor/rich-editor.scss
@@ -17,6 +17,16 @@
pointer-events: none;
}
+ > div:first-child,
+ > details:first-child {
+ // This is hacky, but helps having the leading gapcursor at the right position
+ &.ProseMirror-gapcursor {
+ position: relative;
+ display: block;
+ }
+ margin-top: 0.5rem;
+ }
+
h1,
h2,
h3,
@@ -43,7 +53,6 @@
img {
display: inline-block;
- margin: 0 auto;
max-width: 100%;
&[data-placeholder="true"] {
@@ -119,6 +128,14 @@
display: inline;
padding-top: 0.2rem;
}
+
+ .onebox-wrapper {
+ white-space: normal;
+
+ a {
+ pointer-events: all;
+ }
+ }
}
.d-editor__code-block {
@@ -199,8 +216,38 @@ li.ProseMirror-selectednode:after {
/* Protect against generic img rules */
-img.ProseMirror-separator {
+.ProseMirror-separator {
display: inline !important;
border: none !important;
margin: 0 !important;
}
+
+/*
+ Everything below was copied from prosemirror-gapcursor/style/gapcursor.css
+ */
+
+.ProseMirror-gapcursor {
+ display: none;
+ pointer-events: none;
+ position: absolute;
+}
+
+.ProseMirror-gapcursor:after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: -2px;
+ width: 20px;
+ border-top: 1px solid var(--primary);
+ animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
+}
+
+@keyframes ProseMirror-cursor-blink {
+ to {
+ visibility: hidden;
+ }
+}
+
+.ProseMirror-focused .ProseMirror-gapcursor {
+ display: block;
+}
diff --git a/plugins/discourse-details/assets/javascripts/lib/rich-editor-extension.js b/plugins/discourse-details/assets/javascripts/lib/rich-editor-extension.js
index 7ba4b425789..adc8eb3b5d9 100644
--- a/plugins/discourse-details/assets/javascripts/lib/rich-editor-extension.js
+++ b/plugins/discourse-details/assets/javascripts/lib/rich-editor-extension.js
@@ -1,10 +1,14 @@
export default {
nodeSpec: {
details: {
+ allowGapCursor: true,
attrs: { open: { default: true } },
content: "summary block+",
group: "block",
+ draggable: true,
+ selectable: true,
defining: true,
+ isolating: true,
parseDOM: [{ tag: "details" }],
toDOM: (node) => ["details", { open: node.attrs.open || undefined }, 0],
},
@@ -36,6 +40,9 @@ export default {
},
summary(state, node, parent) {
state.write('[details="');
+ if (node.content.childCount === 0) {
+ state.text(" ");
+ }
node.content.forEach(
(child) =>
child.text &&
diff --git a/plugins/footnote/assets/javascripts/lib/rich-editor-extension.js b/plugins/footnote/assets/javascripts/lib/rich-editor-extension.js
index ff8b4c56321..dc9ca1c0932 100644
--- a/plugins/footnote/assets/javascripts/lib/rich-editor-extension.js
+++ b/plugins/footnote/assets/javascripts/lib/rich-editor-extension.js
@@ -1 +1,42 @@
-export default {};
+export default {
+ nodeSpec: {
+ footnote: {
+ attrs: { id: {} },
+ group: "group",
+ content: "group*",
+ atom: true,
+ draggable: true,
+ selectable: false,
+ parseDOM: [
+ {
+ tag: "span.footnote",
+ preserveWhitespace: "full",
+ getAttrs: (dom) => {
+ return { id: dom.getAttribute("data-id") };
+ },
+ },
+ ],
+ toDOM: (node) => {
+ return ["span", { class: "footnote", "data-id": node.attrs.id }, [0]];
+ },
+ },
+ },
+ parse: {
+ footnote_block: { ignore: true },
+ footnote: {
+ ignore: true,
+ // block: "footnote",
+ // getAttrs: (token, tokens, i) => ({ id: token.meta.id }),
+ },
+ footnote_anchor: { ignore: true, noCloseToken: true },
+ footnote_ref: {
+ node: "footnote",
+ getAttrs: (token, tokens, i) => ({ id: token.meta.id }),
+ },
+ },
+ serializeNode: {
+ footnote: (state, node) => {
+ state.write(`^[${node.attrs.id}] `);
+ },
+ },
+};
diff --git a/plugins/poll/assets/javascripts/lib/rich-editor-extension.js b/plugins/poll/assets/javascripts/lib/rich-editor-extension.js
index 7f996cf51a2..91ee59edfbb 100644
--- a/plugins/poll/assets/javascripts/lib/rich-editor-extension.js
+++ b/plugins/poll/assets/javascripts/lib/rich-editor-extension.js
@@ -2,19 +2,22 @@ export default {
nodeSpec: {
poll: {
attrs: {
- type: {},
- results: {},
- public: {},
+ type: { default: null },
+ results: { default: null },
+ public: { default: null },
name: {},
- chartType: {},
+ chartType: { default: null },
close: { default: null },
groups: { default: null },
max: { default: null },
min: { default: null },
},
- content: "poll_container poll_info",
+ content: "heading? bullet_list poll_info?",
group: "block",
draggable: true,
+ selectable: true,
+ isolating: true,
+ defining: true,
parseDOM: [
{
tag: "div.poll",
@@ -48,17 +51,10 @@ export default {
0,
],
},
- poll_container: {
- content: "heading? bullet_list",
- group: "block",
- parseDOM: [{ tag: "div.poll-container" }],
- toDOM: () => ["div", { class: "poll-container" }, 0],
- },
poll_info: {
content: "inline*",
- group: "block",
- atom: true,
selectable: false,
+ isolating: true,
parseDOM: [{ tag: "div.poll-info" }],
toDOM: () => ["div", { class: "poll-info", contentEditable: false }, 0],
},
@@ -78,13 +74,16 @@ export default {
min: token.attrGet("data-poll-min"),
}),
},
- poll_container: { block: "poll_container" },
+ poll_container: { ignore: true },
poll_title: { block: "heading" },
poll_info: { block: "poll_info" },
poll_info_counts: { ignore: true },
poll_info_counts_count: { ignore: true },
poll_info_number: { ignore: true },
- poll_info_label: { ignore: true },
+ poll_info_label_open(state) {
+ state.addText(" ");
+ },
+ poll_info_label_close() {},
},
serializeNode: {
poll(state, node) {
@@ -96,9 +95,6 @@ export default {
state.renderContent(node);
state.write("[/poll]\n\n");
},
- poll_container(state, node) {
- state.renderContent(node);
- },
poll_info() {},
},
};
diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss
index e62a2bbdf5b..5532c52cb8a 100644
--- a/plugins/poll/assets/stylesheets/common/poll.scss
+++ b/plugins/poll/assets/stylesheets/common/poll.scss
@@ -496,6 +496,7 @@ div.poll-outer {
.d-editor__editable {
.poll {
+ margin-bottom: 1rem;
ul {
list-style-type: none;
padding: 0;
diff --git a/plugins/spoiler-alert/assets/javascripts/lib/rich-editor-extension.js b/plugins/spoiler-alert/assets/javascripts/lib/rich-editor-extension.js
index c85f52aa0cd..ae1c6b4015a 100644
--- a/plugins/spoiler-alert/assets/javascripts/lib/rich-editor-extension.js
+++ b/plugins/spoiler-alert/assets/javascripts/lib/rich-editor-extension.js
@@ -1,4 +1,4 @@
-const INLINE_NODES = ["inline_spoiler", "spoiler"];
+const SPOILER_NODES = ["inline_spoiler", "spoiler"];
export default {
nodeSpec: {
@@ -6,7 +6,6 @@ export default {
attrs: { blurred: { default: true } },
group: "block",
content: "block+",
- defining: true,
parseDOM: [{ tag: "div.spoiled" }],
toDOM: (node) => [
"div",
@@ -51,8 +50,8 @@ export default {
},
plugins: {
props: {
- handleClickOn(view, pos, node, nodePos, event, direct) {
- if (INLINE_NODES.includes(node.type.name)) {
+ handleClickOn(view, pos, node, nodePos) {
+ if (SPOILER_NODES.includes(node.type.name)) {
view.dispatch(
view.state.tr.setNodeMarkup(nodePos, null, {
blurred: !node.attrs.blurred,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 07f6a994a6c..79e46fcd8b9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -356,6 +356,9 @@ importers:
prosemirror-state:
specifier: ^1.4.3
version: 1.4.3
+ prosemirror-transform:
+ specifier: ^1.10.2
+ version: 1.10.2
prosemirror-view:
specifier: ^1.34.3
version: 1.37.1
@@ -542,7 +545,7 @@ importers:
version: 3.0.1(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)))
ember-modifier:
specifier: ^4.2.0
- version: 4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)))
+ version: 4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5))
ember-on-resize-modifier:
specifier: ^2.0.2
version: 2.0.2(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)))(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0))
@@ -783,7 +786,7 @@ importers:
version: 4.2.0
ember-this-fallback:
specifier: ^0.4.0
- version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)))
+ version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5))
devDependencies:
ember-cli:
specifier: ~6.0.1
@@ -1126,7 +1129,7 @@ importers:
version: 5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0))
ember-this-fallback:
specifier: ^0.4.0
- version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)))
+ version: 0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5))
handlebars:
specifier: ^4.7.8
version: 4.7.8
@@ -10118,7 +10121,7 @@ snapshots:
'@glint/template': 1.5.0
optionalDependencies:
ember-cli-htmlbars: 6.3.0
- ember-modifier: 4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)))
+ ember-modifier: 4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5))
'@glint/environment-ember-template-imports@1.5.0(@glint/environment-ember-loose@1.5.0(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0)(ember-modifier@4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5))))(@glint/template@1.5.0)':
dependencies:
@@ -12817,7 +12820,7 @@ snapshots:
- '@babel/core'
- supports-color
- ember-modifier@4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0))):
+ ember-modifier@4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)):
dependencies:
'@embroider/addon-shim': 1.9.0
decorator-transforms: 2.3.0(@babel/core@7.26.0)
@@ -12834,7 +12837,7 @@ snapshots:
ember-auto-import: 2.10.0(@glint/template@1.5.0)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0))
ember-cli-babel: 7.26.11
ember-cli-htmlbars: 5.7.2
- ember-modifier: 4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)))
+ ember-modifier: 4.2.0(@babel/core@7.26.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5))
ember-resize-observer-service: 1.1.0
transitivePeerDependencies:
- '@babel/core'
@@ -13012,7 +13015,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ember-this-fallback@0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0))):
+ ember-this-fallback@0.4.0(patch_hash=znalyv6akdxlqfpmxunrdi3osa)(ember-cli-htmlbars@6.3.0)(ember-source@5.12.0(patch_hash=xx7mvsb7nmshqkkqhmf45r3hse)(@glimmer/component@1.1.2(@babel/core@7.26.0))(@glint/template@1.5.0)(rsvp@4.8.5)):
dependencies:
'@glimmer/syntax': 0.84.3
babel-plugin-ember-template-compilation: 2.2.5