DEV: prosemirror

This commit is contained in:
Renato Atilio 2024-12-21 17:58:53 -03:00
parent e375853ab3
commit 0285d35a80
No known key found for this signature in database
GPG Key ID: CBF93DCB5CBCA1A5
25 changed files with 708 additions and 138 deletions

View File

@ -4,7 +4,7 @@ import loadPluginFeatures from "./features";
import MentionsParser from "./mentions-parser"; import MentionsParser from "./mentions-parser";
import buildOptions from "./options"; import buildOptions from "./options";
function buildEngine(options) { export function buildEngine(options) {
return DiscourseMarkdownIt.withCustomFeatures( return DiscourseMarkdownIt.withCustomFeatures(
loadPluginFeatures() loadPluginFeatures()
).withOptions(buildOptions(options)); ).withOptions(buildOptions(options));

View File

@ -13,12 +13,14 @@ import {
} from "discourse/lib/composer/rich-editor-extensions"; } from "discourse/lib/composer/rich-editor-extensions";
import * as ProsemirrorModel from "prosemirror-model"; import * as ProsemirrorModel from "prosemirror-model";
import * as ProsemirrorView from "prosemirror-view"; 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 { createHighlight } from "../plugins/code-highlight";
import { baseKeymap } from "prosemirror-commands"; import { baseKeymap } from "prosemirror-commands";
import { dropCursor } from "prosemirror-dropcursor"; import { dropCursor } from "prosemirror-dropcursor";
import { history } from "prosemirror-history"; import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap"; import { keymap } from "prosemirror-keymap";
import * as ProsemirrorState from "prosemirror-state";
import { EditorState, Plugin } from "prosemirror-state"; import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view"; import { EditorView } from "prosemirror-view";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
@ -29,12 +31,16 @@ import { convertToMarkdown } from "../lib/serializer";
import { buildInputRules } from "../plugins/inputrules"; import { buildInputRules } from "../plugins/inputrules";
import { buildKeymap } from "../plugins/keymap"; import { buildKeymap } from "../plugins/keymap";
import placeholder from "../plugins/placeholder"; import placeholder from "../plugins/placeholder";
import { gapCursor } from "prosemirror-gapcursor";
export default class ProsemirrorEditor extends Component { export default class ProsemirrorEditor extends Component {
@service appEvents; @service appEvents;
@service menu; @service menu;
@service siteSettings; @service siteSettings;
@service dialog;
@tracked rootElement; @tracked rootElement;
editorContainerId = guidFor(this); editorContainerId = guidFor(this);
schema = createSchema(); schema = createSchema();
view; view;
@ -59,18 +65,11 @@ export default class ProsemirrorEditor extends Component {
keymap(buildKeymap(this.schema, keymapFromArgs)), keymap(buildKeymap(this.schema, keymapFromArgs)),
keymap(baseKeymap), keymap(baseKeymap),
dropCursor({ color: "var(--primary)" }), dropCursor({ color: "var(--primary)" }),
gapCursor(),
history(), history(),
placeholder(this.args.placeholder), placeholder(this.args.placeholder),
createHighlight(), createHighlight(),
...getPlugins().map((plugin) => ...getPlugins().flatMap(processPlugin),
typeof plugin === "function"
? plugin({
...ProsemirrorState,
...ProsemirrorModel,
...ProsemirrorView,
})
: new Plugin(plugin)
),
]; ];
this.state = EditorState.create({ this.state = EditorState.create({
@ -126,9 +125,14 @@ export default class ProsemirrorEditor extends Component {
@bind @bind
convertFromValue() { convertFromValue() {
const doc = convertFromMarkdown(this.schema, this.args.value); let doc;
try {
// console.log("Resulting doc:", doc); doc = convertFromMarkdown(this.schema, this.args.value);
} catch (e) {
console.error(e);
this.dialog.alert(e.message);
return;
}
const tr = this.state.tr const tr = this.state.tr
.replaceWith(0, this.state.doc.content.size, doc.content) .replaceWith(0, this.state.doc.content.size, doc.content)
@ -151,3 +155,19 @@ export default class ProsemirrorEditor extends Component {
</div> </div>
</template> </template>
} }
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);
}

View File

@ -33,8 +33,6 @@ class CodeBlockWithLangSelectorNodeView {
this.dom.appendChild(select); this.dom.appendChild(select);
// TODO(renato): leaving with the keyboard to before the node doesn't work
const code = document.createElement("code"); const code = document.createElement("code");
this.dom.appendChild(document.createElement("pre")).appendChild(code); this.dom.appendChild(document.createElement("pre")).appendChild(code);
this.contentDOM = code; this.contentDOM = code;

View File

@ -1,7 +1,9 @@
import { buildEmojiUrl, emojiExists, isCustomEmoji } from "pretty-text/emoji"; import { buildEmojiUrl, emojiExists, isCustomEmoji } from "pretty-text/emoji";
import { translations } from "pretty-text/emoji/data";
import { emojiOptions } from "discourse/lib/text"; 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 { export default {
nodeSpec: { nodeSpec: {
emoji: { emoji: {
@ -52,6 +54,18 @@ export default {
}, },
options: { undoable: false }, 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: { parse: {
@ -64,7 +78,11 @@ export default {
}, },
serializeNode: { serializeNode: {
emoji: (state, node) => { emoji(state, node) {
if (!isBoundary(state.out, state.out.length - 1)) {
state.write(" ");
}
state.write(`:${node.attrs.code}:`); state.write(`:${node.attrs.code}:`);
}, },
}, },

View File

@ -1,3 +1,5 @@
import { isBoundary } from "discourse/static/prosemirror/lib/markdown-it";
export default { export default {
nodeSpec: { nodeSpec: {
hashtag: { hashtag: {
@ -30,7 +32,7 @@ export default {
inputRules: [ 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) => handler: (state, match, start, end) =>
state.selection.$from.nodeBefore?.type !== state.schema.nodes.hashtag && state.selection.$from.nodeBefore?.type !== state.schema.nodes.hashtag &&
state.tr.replaceWith(start, end, [ state.tr.replaceWith(start, end, [
@ -42,7 +44,7 @@ export default {
], ],
parse: { parse: {
span: (state, token, tokens, i) => { span(state, token, tokens, i) {
if (token.attrGet("class") === "hashtag-raw") { if (token.attrGet("class") === "hashtag-raw") {
state.openNode(state.schema.nodes.hashtag, { state.openNode(state.schema.nodes.hashtag, {
name: tokens[i + 1].content.slice(1), name: tokens[i + 1].content.slice(1),
@ -53,8 +55,18 @@ export default {
}, },
serializeNode: { serializeNode: {
hashtag: (state, node) => { hashtag(state, node, parent, index) {
if (!isBoundary(state.out, state.out.length - 1)) {
state.write(" ");
}
state.write(`#${node.attrs.name}`); 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(" ");
}
}, },
}, },
}; };

View File

@ -1,6 +1,47 @@
export default { export default {
nodeSpec: { nodeSpec: {
// TODO(renato): html_block should be like a passthrough code block html_block: {
html_block: { block: "paragraph", noCloseToken: true }, 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);
},
}, },
}; };

View File

@ -28,6 +28,7 @@ export default {
html_inline: { html_inline: {
group: "inline", group: "inline",
inline: true, inline: true,
isolating: true,
content: "inline*", content: "inline*",
attrs: { tag: {} }, attrs: { tag: {} },
parseDOM: ALLOWED_INLINE.map((tag) => ({ tag })), parseDOM: ALLOWED_INLINE.map((tag) => ({ tag })),
@ -37,8 +38,8 @@ export default {
parse: { parse: {
// TODO(renato): it breaks if it's missing an end tag // TODO(renato): it breaks if it's missing an end tag
html_inline: (state, token) => { html_inline: (state, token) => {
const openMatch = token.content.match(/^<([a-z]+)>$/u); const openMatch = token.content.match(/^<([a-z]+)>$/);
const closeMatch = token.content.match(/^<\/([a-z]+)>$/u); const closeMatch = token.content.match(/^<\/([a-z]+)>$/);
if (openMatch) { if (openMatch) {
const tagName = openMatch[1]; const tagName = openMatch[1];

View File

@ -18,8 +18,8 @@ import typographerReplacements from "./typographer-replacements";
import underline from "./underline"; import underline from "./underline";
const defaultExtensions = [ const defaultExtensions = [
// emoji before image
emoji, emoji,
// image must be after emoji
image, image,
hashtag, hashtag,
mention, mention,
@ -27,16 +27,17 @@ const defaultExtensions = [
underline, underline,
htmlInline, htmlInline,
htmlBlock, htmlBlock,
// onebox before link
onebox,
link, link,
heading, heading,
codeBlock, codeBlock,
quote, quote,
onebox,
trailingParagraph, trailingParagraph,
typographerReplacements, typographerReplacements,
markdownPaste, markdownPaste,
// table must be last // table last
table, table,
]; ];

View File

@ -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 { export default {
inputRules: [ markSpec: {
// []() replacement link: {
({ schema, markInputRule }) => attrs: {
markInputRule( href: {},
/\[([^\]]+)]\(([^)\s]+)(?:\s+[“"']([^“"']+)[”"'])?\)$/, title: { default: null },
schema.marks.link, autoLink: { default: null },
(match) => { },
return { href: match[2], title: match[3] }; inclusive: false,
} parseDOM: [
), {
// TODO(renato): auto-linkify when typing (https://github.com/markdown-it/markdown-it/blob/master/lib/rules_inline/autolink.mjs) tag: "a[href]",
], getAttrs(dom) {
plugins: ({ Plugin, Slice, Fragment }) => 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({ 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: { props: {
// Auto-linkify plain-text pasted URLs // 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) { clipboardTextParser(text, $context, plain, view) {
// TODO(renato): a less naive regex, reuse existing if (view.state.selection.empty || !getLinkify().test(text)) {
if (!text.match(/^https?:\/\//) || view.state.selection.empty) {
return; return;
} }
@ -34,6 +123,119 @@ export default {
]); ]);
return new Slice(Fragment.from(textNode), 0, 0); 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,
}));
}

View File

@ -1,5 +1,3 @@
// TODO(renato): similar to emoji, avoid joining anything@mentions, as it's invalid markdown
import { mentionRegex } from "pretty-text/mentions"; import { mentionRegex } from "pretty-text/mentions";
export default { export default {

View File

@ -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 { 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 { export default {
nodeSpec: { nodeSpec: {
onebox: { onebox: {
attrs: { url: {}, html: {} }, attrs: { url: {}, html: {} },
selectable: false, selectable: true,
group: "inline", group: "block",
inline: true,
atom: true, atom: true,
draggable: true, draggable: true,
parseDOM: [ parseDOM: [
@ -19,18 +25,58 @@ export default {
}, },
], ],
toDOM(node) { toDOM(node) {
// const dom = document.createElement("aside"); const dom = document.createElement("div");
// dom.outerHTML = node.attrs.html; dom.classList.add("onebox-wrapper");
dom.innerHTML = node.attrs.html;
// TODO(renato): revisit? return dom;
return new DOMParser().parseFromString(node.attrs.html, "text/html") },
.body.firstChild; },
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: { serializeNode: {
onebox(state, node) { 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({ const plugin = new Plugin({
state: { state: {
init() { init() {
return []; return { full: {}, inline: {} };
}, },
apply(tr, value) { apply(tr, value) {
// TODO(renato) const updated = { full: [], inline: [] };
return value;
// 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() { view() {
return { return {
update(view, prevState) { async update(view, prevState) {
if (prevState.doc.eq(view.state.doc)) { if (prevState.doc.eq(view.state.doc)) {
return; 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 <a href="url">url</a> onebox response
if (
new RegExp(
`<a href=["']${escapeRegExp(url)}["'].*>${escapeRegExp(
url
)}</a>`
).test(html)
) {
continue;
}
for (const unresolved of unresolvedLinks) { const tr = view.state.tr;
const isInline = unresolved.isInline; for (const pos of list) {
// console.log(isInline, cachedInlineOnebox(unresolved.text)); 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 const inlineOneboxes = await loadInlineOneboxes(
? "onebox-loading" Object.keys(inline),
: "inline-onebox-loading"; view.props.discourse
);
if (!isInline) { for (const [url, onebox] of Object.entries(inlineOneboxes)) {
// console.log(lookupCache(unresolved.text)); 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) { async function loadInlineOneboxes(urls, { categoryId, topicId }) {
try { const allOneboxes = {};
new URL(text); // If it can be parsed as a URL, it's valid.
return true; const uncachedUrls = [];
} catch { for (const url of urls) {
return false; 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) { async function loadFullOnebox(url, { categoryId, topicId }) {
const resolvedPos = state.doc.resolve(pos); const cached = lookupCache(url);
const parent = resolvedPos.parent; if (cached) {
return cached;
}
return parent.childCount !== 1; return new Promise((onResolve) => {
addToLoadingQueue({ url, categoryId, topicId, onResolve });
loadNext(ajax);
});
} }

View File

@ -17,6 +17,8 @@ export default {
group: "block", group: "block",
tableRole: "table", tableRole: "table",
isolating: true, isolating: true,
selectable: true,
draggable: true,
parseDOM: [{ tag: "table" }], parseDOM: [{ tag: "table" }],
toDOM() { toDOM() {
return ["table", 0]; return ["table", 0];
@ -124,10 +126,13 @@ export default {
table(state, node) { table(state, node) {
state.flushClose(1); state.flushClose(1);
let headerBuffer = state.delim && state.atBlank() ? state.delim : ""; let headerBuffer = state.delim;
const prevInTable = state.inTable; const prevInTable = state.inTable;
state.inTable = true; 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 // group is table_head or table_body
node.forEach((group, groupOffset, groupIndex) => { node.forEach((group, groupOffset, groupIndex) => {
group.forEach((row) => { group.forEach((row) => {

View File

@ -16,16 +16,27 @@ export default {
}, },
state: { state: {
init(_, state) { init(_, state) {
return state.doc.lastChild.type !== state.schema.nodes.paragraph; return !isLastChildEmptyParagraph(state);
}, },
apply(tr, value) { apply(tr, value) {
if (!tr.docChanged) { if (!tr.docChanged) {
return value; 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
);
}

View File

@ -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));

View File

@ -1,8 +1,6 @@
import { defaultMarkdownParser, MarkdownParser } from "prosemirror-markdown"; import { defaultMarkdownParser, MarkdownParser } from "prosemirror-markdown";
import { getParsers } from "discourse/lib/composer/rich-editor-extensions"; import { getParsers } from "discourse/lib/composer/rich-editor-extensions";
import { parse as markdownItParse } from "discourse/static/markdown-it"; import { parse } from "./markdown-it";
import loadPluginFeatures from "discourse/static/markdown-it/features";
import defaultFeatures from "discourse-markdown-it/features/index";
// TODO(renato): We need a workaround for this parsing issue: // TODO(renato): We need a workaround for this parsing issue:
// https://github.com/ProseMirror/prosemirror-markdown/issues/82 // https://github.com/ProseMirror/prosemirror-markdown/issues/82
@ -19,9 +17,9 @@ const postParseTokens = {
softbreak: (state) => state.addNode(state.schema.nodes.hard_break), softbreak: (state) => state.addNode(state.schema.nodes.hard_break),
}; };
let parseOptions; let initialized;
function initializeParser() { function loadCustomParsers() {
if (parseOptions) { if (initialized) {
return; return;
} }
@ -33,18 +31,13 @@ function initializeParser() {
} }
} }
const featuresOverride = [...defaultFeatures, ...loadPluginFeatures()] initialized = true;
.map(({ id }) => id)
// Avoid oneboxing when parsing, we'll handle that separately
.filter((id) => id !== "onebox");
parseOptions = { featuresOverride };
} }
export function convertFromMarkdown(schema, text) { export function convertFromMarkdown(schema, text) {
initializeParser(); loadCustomParsers();
const tokens = markdownItParse(text, parseOptions); const tokens = parse(text);
console.log("Converting tokens", tokens); console.log("Converting tokens", tokens);

View File

@ -1,8 +1,6 @@
import { import {
chainCommands, chainCommands,
exitCode, exitCode,
joinDown,
joinUp,
lift, lift,
selectParentNode, selectParentNode,
setBlockType, setBlockType,
@ -48,8 +46,6 @@ export function buildKeymap(schema, initialKeymap = {}, suppressKeys) {
bind("Mod-y", redo); bind("Mod-y", redo);
} }
bind("Alt-ArrowUp", joinUp);
bind("Alt-ArrowDown", joinDown);
bind("Mod-BracketLeft", lift); bind("Mod-BracketLeft", lift);
bind("Escape", selectParentNode); bind("Escape", selectParentNode);

View File

@ -36,6 +36,7 @@
"pretty-text": "workspace:1.0.0", "pretty-text": "workspace:1.0.0",
"prosemirror-commands": "^1.6.0", "prosemirror-commands": "^1.6.0",
"prosemirror-dropcursor": "^1.8.1", "prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-highlight": "^0.11.0", "prosemirror-highlight": "^0.11.0",
"prosemirror-history": "^1.4.1", "prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.4.0", "prosemirror-inputrules": "^1.4.0",
@ -44,6 +45,7 @@
"prosemirror-model": "^1.23.0", "prosemirror-model": "^1.23.0",
"prosemirror-schema-list": "^1.4.1", "prosemirror-schema-list": "^1.4.1",
"prosemirror-state": "^1.4.3", "prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.34.3" "prosemirror-view": "^1.34.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -54,7 +54,7 @@ function _handleLoadingOneboxImages() {
this.removeEventListener("load", _handleLoadingOneboxImages); this.removeEventListener("load", _handleLoadingOneboxImages);
} }
function loadNext(ajax) { export function loadNext(ajax) {
if (loadingQueue.length === 0) { if (loadingQueue.length === 0) {
timeout = null; timeout = null;
return; return;
@ -62,7 +62,8 @@ function loadNext(ajax) {
let timeoutMs = 150; let timeoutMs = 150;
let removeLoading = true; let removeLoading = true;
const { url, refresh, elem, categoryId, topicId } = loadingQueue.shift(); const { url, refresh, elem, categoryId, topicId, onResolve } =
loadingQueue.shift();
// Retrieve the onebox // Retrieve the onebox
return ajax("/onebox", { return ajax("/onebox", {
@ -78,6 +79,7 @@ function loadNext(ajax) {
(template) => { (template) => {
const node = domFromString(template)[0]; const node = domFromString(template)[0];
setLocalCache(normalize(url), node); setLocalCache(normalize(url), node);
onResolve?.(template);
elem.replaceWith(node); elem.replaceWith(node);
applySquareGenericOnebox(node); applySquareGenericOnebox(node);
}, },
@ -155,3 +157,17 @@ export function load({
timeout = timeout || discourseLater(() => loadNext(ajax), 150); 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 });
}

View File

@ -17,6 +17,16 @@
pointer-events: none; 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, h1,
h2, h2,
h3, h3,
@ -43,7 +53,6 @@
img { img {
display: inline-block; display: inline-block;
margin: 0 auto;
max-width: 100%; max-width: 100%;
&[data-placeholder="true"] { &[data-placeholder="true"] {
@ -119,6 +128,14 @@
display: inline; display: inline;
padding-top: 0.2rem; padding-top: 0.2rem;
} }
.onebox-wrapper {
white-space: normal;
a {
pointer-events: all;
}
}
} }
.d-editor__code-block { .d-editor__code-block {
@ -199,8 +216,38 @@ li.ProseMirror-selectednode:after {
/* Protect against generic img rules */ /* Protect against generic img rules */
img.ProseMirror-separator { .ProseMirror-separator {
display: inline !important; display: inline !important;
border: none !important; border: none !important;
margin: 0 !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;
}

View File

@ -1,10 +1,14 @@
export default { export default {
nodeSpec: { nodeSpec: {
details: { details: {
allowGapCursor: true,
attrs: { open: { default: true } }, attrs: { open: { default: true } },
content: "summary block+", content: "summary block+",
group: "block", group: "block",
draggable: true,
selectable: true,
defining: true, defining: true,
isolating: true,
parseDOM: [{ tag: "details" }], parseDOM: [{ tag: "details" }],
toDOM: (node) => ["details", { open: node.attrs.open || undefined }, 0], toDOM: (node) => ["details", { open: node.attrs.open || undefined }, 0],
}, },
@ -36,6 +40,9 @@ export default {
}, },
summary(state, node, parent) { summary(state, node, parent) {
state.write('[details="'); state.write('[details="');
if (node.content.childCount === 0) {
state.text(" ");
}
node.content.forEach( node.content.forEach(
(child) => (child) =>
child.text && child.text &&

View File

@ -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}] `);
},
},
};

View File

@ -2,19 +2,22 @@ export default {
nodeSpec: { nodeSpec: {
poll: { poll: {
attrs: { attrs: {
type: {}, type: { default: null },
results: {}, results: { default: null },
public: {}, public: { default: null },
name: {}, name: {},
chartType: {}, chartType: { default: null },
close: { default: null }, close: { default: null },
groups: { default: null }, groups: { default: null },
max: { default: null }, max: { default: null },
min: { default: null }, min: { default: null },
}, },
content: "poll_container poll_info", content: "heading? bullet_list poll_info?",
group: "block", group: "block",
draggable: true, draggable: true,
selectable: true,
isolating: true,
defining: true,
parseDOM: [ parseDOM: [
{ {
tag: "div.poll", tag: "div.poll",
@ -48,17 +51,10 @@ export default {
0, 0,
], ],
}, },
poll_container: {
content: "heading? bullet_list",
group: "block",
parseDOM: [{ tag: "div.poll-container" }],
toDOM: () => ["div", { class: "poll-container" }, 0],
},
poll_info: { poll_info: {
content: "inline*", content: "inline*",
group: "block",
atom: true,
selectable: false, selectable: false,
isolating: true,
parseDOM: [{ tag: "div.poll-info" }], parseDOM: [{ tag: "div.poll-info" }],
toDOM: () => ["div", { class: "poll-info", contentEditable: false }, 0], toDOM: () => ["div", { class: "poll-info", contentEditable: false }, 0],
}, },
@ -78,13 +74,16 @@ export default {
min: token.attrGet("data-poll-min"), min: token.attrGet("data-poll-min"),
}), }),
}, },
poll_container: { block: "poll_container" }, poll_container: { ignore: true },
poll_title: { block: "heading" }, poll_title: { block: "heading" },
poll_info: { block: "poll_info" }, poll_info: { block: "poll_info" },
poll_info_counts: { ignore: true }, poll_info_counts: { ignore: true },
poll_info_counts_count: { ignore: true }, poll_info_counts_count: { ignore: true },
poll_info_number: { ignore: true }, poll_info_number: { ignore: true },
poll_info_label: { ignore: true }, poll_info_label_open(state) {
state.addText(" ");
},
poll_info_label_close() {},
}, },
serializeNode: { serializeNode: {
poll(state, node) { poll(state, node) {
@ -96,9 +95,6 @@ export default {
state.renderContent(node); state.renderContent(node);
state.write("[/poll]\n\n"); state.write("[/poll]\n\n");
}, },
poll_container(state, node) {
state.renderContent(node);
},
poll_info() {}, poll_info() {},
}, },
}; };

View File

@ -496,6 +496,7 @@ div.poll-outer {
.d-editor__editable { .d-editor__editable {
.poll { .poll {
margin-bottom: 1rem;
ul { ul {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;

View File

@ -1,4 +1,4 @@
const INLINE_NODES = ["inline_spoiler", "spoiler"]; const SPOILER_NODES = ["inline_spoiler", "spoiler"];
export default { export default {
nodeSpec: { nodeSpec: {
@ -6,7 +6,6 @@ export default {
attrs: { blurred: { default: true } }, attrs: { blurred: { default: true } },
group: "block", group: "block",
content: "block+", content: "block+",
defining: true,
parseDOM: [{ tag: "div.spoiled" }], parseDOM: [{ tag: "div.spoiled" }],
toDOM: (node) => [ toDOM: (node) => [
"div", "div",
@ -51,8 +50,8 @@ export default {
}, },
plugins: { plugins: {
props: { props: {
handleClickOn(view, pos, node, nodePos, event, direct) { handleClickOn(view, pos, node, nodePos) {
if (INLINE_NODES.includes(node.type.name)) { if (SPOILER_NODES.includes(node.type.name)) {
view.dispatch( view.dispatch(
view.state.tr.setNodeMarkup(nodePos, null, { view.state.tr.setNodeMarkup(nodePos, null, {
blurred: !node.attrs.blurred, blurred: !node.attrs.blurred,

View File

@ -356,6 +356,9 @@ importers:
prosemirror-state: prosemirror-state:
specifier: ^1.4.3 specifier: ^1.4.3
version: 1.4.3 version: 1.4.3
prosemirror-transform:
specifier: ^1.10.2
version: 1.10.2
prosemirror-view: prosemirror-view:
specifier: ^1.34.3 specifier: ^1.34.3
version: 1.37.1 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))) 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: ember-modifier:
specifier: ^4.2.0 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: ember-on-resize-modifier:
specifier: ^2.0.2 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)) 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 version: 4.2.0
ember-this-fallback: ember-this-fallback:
specifier: ^0.4.0 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: devDependencies:
ember-cli: ember-cli:
specifier: ~6.0.1 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)) 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: ember-this-fallback:
specifier: ^0.4.0 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: handlebars:
specifier: ^4.7.8 specifier: ^4.7.8
version: 4.7.8 version: 4.7.8
@ -10118,7 +10121,7 @@ snapshots:
'@glint/template': 1.5.0 '@glint/template': 1.5.0
optionalDependencies: optionalDependencies:
ember-cli-htmlbars: 6.3.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)(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)': '@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: dependencies:
@ -12817,7 +12820,7 @@ snapshots:
- '@babel/core' - '@babel/core'
- supports-color - 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: dependencies:
'@embroider/addon-shim': 1.9.0 '@embroider/addon-shim': 1.9.0
decorator-transforms: 2.3.0(@babel/core@7.26.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-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-babel: 7.26.11
ember-cli-htmlbars: 5.7.2 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 ember-resize-observer-service: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@ -13012,7 +13015,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@glimmer/syntax': 0.84.3 '@glimmer/syntax': 0.84.3
babel-plugin-ember-template-compilation: 2.2.5 babel-plugin-ember-template-compilation: 2.2.5