Skip to content

Commit ef8be2a

Browse files
committed
bugfix: typing table markdown
1 parent 9576fe0 commit ef8be2a

2 files changed

Lines changed: 197 additions & 91 deletions

File tree

src/blazer-markdown-editor.js

Lines changed: 110 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -180,63 +180,111 @@ function buildBlockInputRules() {
180180
}
181181

182182
function tableInputRulePlugin() {
183-
return new Plugin({
184-
props: {
185-
handleKeyDown(view, event) {
186-
if (event.key !== "Enter") return false;
187-
const { state } = view;
188-
const { $head } = state.selection;
189-
const parentNode = $head.parent;
190-
if (parentNode.type.name !== "paragraph") return false;
183+
function parseHeaderCells(text) {
184+
if (!text.match(/^\s*\|.+\|\s*$/)) return null;
185+
const cells = text
186+
.replace(/^\s*\|/, "")
187+
.replace(/\|\s*$/, "")
188+
.split("|")
189+
.map((cell) => cell.trim());
190+
return cells.length >= 2 ? cells : null;
191+
}
191192

192-
const currentLine = parentNode.textContent;
193-
if (!currentLine.match(/^\s*\|[\s:]*-{3,}[\s:]*(?:\|[\s:]*-{3,}[\s:]*)+\|?\s*$/)) return false;
193+
function parseSeparatorCells(text) {
194+
if (!text.match(/^\s*\|[\s:]*-{3,}[\s:]*(?:\|[\s:]*-{3,}[\s:]*)+\|?\s*$/)) return null;
195+
const cells = text
196+
.replace(/^\s*\|/, "")
197+
.replace(/\|\s*$/, "")
198+
.split("|")
199+
.map((cell) => cell.trim());
194200

195-
const grandParent = $head.node($head.depth - 1);
196-
const myIndex = $head.index($head.depth - 1);
197-
if (myIndex === 0) return false;
201+
if (!cells.every((cell) => /^:?-{3,}:?$/.test(cell))) return null;
202+
return cells;
203+
}
198204

199-
const headerBlock = grandParent.child(myIndex - 1);
200-
if (headerBlock.type.name !== "paragraph") return false;
205+
function getAlignments(separatorCells) {
206+
return separatorCells.map((cell) => {
207+
const left = cell.startsWith(":");
208+
const right = cell.endsWith(":");
209+
if (left && right) return "center";
210+
if (right) return "right";
211+
return null;
212+
});
213+
}
201214

202-
const headerText = headerBlock.textContent;
203-
if (!headerText.match(/^\s*\|.+\|/)) return false;
215+
function buildTableNode(headerCells, aligns) {
216+
const headerRow = schema.nodes.table_row.create(
217+
null,
218+
headerCells.map((text, i) =>
219+
schema.nodes.table_header.create(
220+
{ alignment: aligns[i] || null },
221+
text ? [schema.node("paragraph", null, [schema.text(text)])] : [schema.node("paragraph")],
222+
),
223+
),
224+
);
225+
226+
const emptyRow = schema.nodes.table_row.create(
227+
null,
228+
headerCells.map((_, i) => schema.nodes.table_cell.create({ alignment: aligns[i] || null }, [schema.node("paragraph")])),
229+
);
230+
231+
return schema.nodes.table.create(null, [headerRow, emptyRow]);
232+
}
204233

205-
const headerCells = headerText.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
206-
const sepCells = currentLine.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
207-
if (headerCells.length < 2 || sepCells.length !== headerCells.length) return false;
234+
function findTableMarkdownRange(state) {
235+
const { $head } = state.selection;
236+
const paragraph = $head.parent;
237+
if (paragraph.type.name !== "paragraph") return null;
208238

209-
const aligns = sepCells.map((s) => {
210-
const left = s.startsWith(":");
211-
const right = s.endsWith(":");
212-
if (left && right) return "center";
213-
if (right) return "right";
214-
return null;
215-
});
239+
const paragraphDepth = $head.depth;
240+
const paragraphFrom = $head.before(paragraphDepth);
241+
const paragraphTo = $head.after(paragraphDepth);
242+
243+
const text = paragraph.textContent;
244+
const lines = text.split("\n");
245+
246+
if (lines.length === 2) {
247+
const headerCells = parseHeaderCells(lines[0]);
248+
const separatorCells = parseSeparatorCells(lines[1]);
249+
if (!headerCells || !separatorCells || separatorCells.length !== headerCells.length) return null;
250+
return {
251+
from: paragraphFrom,
252+
to: paragraphTo,
253+
headerCells,
254+
aligns: getAlignments(separatorCells),
255+
};
256+
}
257+
258+
const containerDepth = paragraphDepth - 1;
259+
const container = $head.node(containerDepth);
260+
const paragraphIndex = $head.index(containerDepth);
261+
if (paragraphIndex === 0) return null;
216262

217-
const headerRow = schema.nodes.table_row.create(
218-
null,
219-
headerCells.map((text, i) =>
220-
schema.nodes.table_header.create(
221-
{ alignment: aligns[i] || null },
222-
text ? [schema.node("paragraph", null, [schema.text(text)])] : [schema.node("paragraph")],
223-
),
224-
),
225-
);
226-
227-
const emptyRow = schema.nodes.table_row.create(
228-
null,
229-
headerCells.map((_, i) => schema.nodes.table_cell.create({ alignment: aligns[i] || null }, [schema.node("paragraph")])),
230-
);
231-
232-
const table = schema.nodes.table.create(null, [headerRow, emptyRow]);
233-
234-
let pos = $head.before($head.depth - 1);
235-
for (let i = 0; i < myIndex - 1; i++) pos += grandParent.child(i).nodeSize;
236-
const hStart = pos;
237-
const sepEnd = hStart + headerBlock.nodeSize + parentNode.nodeSize;
238-
239-
const tr = state.tr.replaceWith(hStart, sepEnd, table);
263+
const headerBlock = container.child(paragraphIndex - 1);
264+
if (headerBlock.type.name !== "paragraph") return null;
265+
266+
const headerCells = parseHeaderCells(headerBlock.textContent);
267+
const separatorCells = parseSeparatorCells(text);
268+
if (!headerCells || !separatorCells || separatorCells.length !== headerCells.length) return null;
269+
270+
return {
271+
from: paragraphFrom - headerBlock.nodeSize,
272+
to: paragraphTo,
273+
headerCells,
274+
aligns: getAlignments(separatorCells),
275+
};
276+
}
277+
278+
return new Plugin({
279+
props: {
280+
handleKeyDown(view, event) {
281+
if (event.key !== "Enter") return false;
282+
const { state } = view;
283+
const tableRange = findTableMarkdownRange(state);
284+
if (!tableRange) return false;
285+
286+
const table = buildTableNode(tableRange.headerCells, tableRange.aligns);
287+
const tr = state.tr.replaceWith(tableRange.from, tableRange.to, table);
240288
view.dispatch(tr);
241289
event.preventDefault();
242290
return true;
@@ -245,6 +293,17 @@ function tableInputRulePlugin() {
245293
});
246294
}
247295

296+
function compactLineBreakKeymap() {
297+
return keymap({
298+
"Mod-Enter": (state, dispatch) => {
299+
const { $head } = state.selection;
300+
if ($head.parent.type.name !== "paragraph") return false;
301+
if (dispatch) dispatch(state.tr.insertText("\n"));
302+
return true;
303+
},
304+
});
305+
}
306+
248307
// ── Inline Markdown Input Rules ──
249308
const inlineMarkdownRules = inputRules({ rules: [
250309
// **bold**
@@ -524,6 +583,7 @@ function createPlugins() {
524583
keymap(baseKeymap),
525584
buildBlockInputRules(),
526585
inlineMarkdownRules,
586+
compactLineBreakKeymap(),
527587
codeBlockEscapeKeymap(),
528588
columnResizing(),
529589
tableEditing(),

wwwroot/blazer-markdown-editor.js

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -31813,52 +31813,87 @@
3181331813
return inputRules({ rules });
3181431814
}
3181531815
function tableInputRulePlugin() {
31816+
function parseHeaderCells(text2) {
31817+
if (!text2.match(/^\s*\|.+\|\s*$/)) return null;
31818+
const cells = text2.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
31819+
return cells.length >= 2 ? cells : null;
31820+
}
31821+
function parseSeparatorCells(text2) {
31822+
if (!text2.match(/^\s*\|[\s:]*-{3,}[\s:]*(?:\|[\s:]*-{3,}[\s:]*)+\|?\s*$/)) return null;
31823+
const cells = text2.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
31824+
if (!cells.every((cell) => /^:?-{3,}:?$/.test(cell))) return null;
31825+
return cells;
31826+
}
31827+
function getAlignments(separatorCells) {
31828+
return separatorCells.map((cell) => {
31829+
const left = cell.startsWith(":");
31830+
const right = cell.endsWith(":");
31831+
if (left && right) return "center";
31832+
if (right) return "right";
31833+
return null;
31834+
});
31835+
}
31836+
function buildTableNode(headerCells, aligns) {
31837+
const headerRow = schema2.nodes.table_row.create(
31838+
null,
31839+
headerCells.map(
31840+
(text2, i) => schema2.nodes.table_header.create(
31841+
{ alignment: aligns[i] || null },
31842+
text2 ? [schema2.node("paragraph", null, [schema2.text(text2)])] : [schema2.node("paragraph")]
31843+
)
31844+
)
31845+
);
31846+
const emptyRow = schema2.nodes.table_row.create(
31847+
null,
31848+
headerCells.map((_, i) => schema2.nodes.table_cell.create({ alignment: aligns[i] || null }, [schema2.node("paragraph")]))
31849+
);
31850+
return schema2.nodes.table.create(null, [headerRow, emptyRow]);
31851+
}
31852+
function findTableMarkdownRange(state) {
31853+
const { $head } = state.selection;
31854+
const paragraph = $head.parent;
31855+
if (paragraph.type.name !== "paragraph") return null;
31856+
const paragraphDepth = $head.depth;
31857+
const paragraphFrom = $head.before(paragraphDepth);
31858+
const paragraphTo = $head.after(paragraphDepth);
31859+
const text2 = paragraph.textContent;
31860+
const lines = text2.split("\n");
31861+
if (lines.length === 2) {
31862+
const headerCells = parseHeaderCells(lines[0]);
31863+
const separatorCells = parseSeparatorCells(lines[1]);
31864+
if (!headerCells || !separatorCells || separatorCells.length !== headerCells.length) return null;
31865+
return {
31866+
from: paragraphFrom,
31867+
to: paragraphTo,
31868+
headerCells,
31869+
aligns: getAlignments(separatorCells)
31870+
};
31871+
}
31872+
const containerDepth = paragraphDepth - 1;
31873+
const container = $head.node(containerDepth);
31874+
const paragraphIndex = $head.index(containerDepth);
31875+
if (paragraphIndex === 0) return null;
31876+
const headerBlock = container.child(paragraphIndex - 1);
31877+
if (headerBlock.type.name !== "paragraph") return null;
31878+
const headerCells = parseHeaderCells(headerBlock.textContent);
31879+
const separatorCells = parseSeparatorCells(text2);
31880+
if (!headerCells || !separatorCells || separatorCells.length !== headerCells.length) return null;
31881+
return {
31882+
from: paragraphFrom - headerBlock.nodeSize,
31883+
to: paragraphTo,
31884+
headerCells,
31885+
aligns: getAlignments(separatorCells)
31886+
};
31887+
}
3181631888
return new Plugin({
3181731889
props: {
3181831890
handleKeyDown(view, event) {
3181931891
if (event.key !== "Enter") return false;
3182031892
const { state } = view;
31821-
const { $head } = state.selection;
31822-
const parentNode3 = $head.parent;
31823-
if (parentNode3.type.name !== "paragraph") return false;
31824-
const currentLine = parentNode3.textContent;
31825-
if (!currentLine.match(/^\s*\|[\s:]*-{3,}[\s:]*(?:\|[\s:]*-{3,}[\s:]*)+\|?\s*$/)) return false;
31826-
const grandParent = $head.node($head.depth - 1);
31827-
const myIndex = $head.index($head.depth - 1);
31828-
if (myIndex === 0) return false;
31829-
const headerBlock = grandParent.child(myIndex - 1);
31830-
if (headerBlock.type.name !== "paragraph") return false;
31831-
const headerText = headerBlock.textContent;
31832-
if (!headerText.match(/^\s*\|.+\|/)) return false;
31833-
const headerCells = headerText.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
31834-
const sepCells = currentLine.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
31835-
if (headerCells.length < 2 || sepCells.length !== headerCells.length) return false;
31836-
const aligns = sepCells.map((s) => {
31837-
const left = s.startsWith(":");
31838-
const right = s.endsWith(":");
31839-
if (left && right) return "center";
31840-
if (right) return "right";
31841-
return null;
31842-
});
31843-
const headerRow = schema2.nodes.table_row.create(
31844-
null,
31845-
headerCells.map(
31846-
(text2, i) => schema2.nodes.table_header.create(
31847-
{ alignment: aligns[i] || null },
31848-
text2 ? [schema2.node("paragraph", null, [schema2.text(text2)])] : [schema2.node("paragraph")]
31849-
)
31850-
)
31851-
);
31852-
const emptyRow = schema2.nodes.table_row.create(
31853-
null,
31854-
headerCells.map((_, i) => schema2.nodes.table_cell.create({ alignment: aligns[i] || null }, [schema2.node("paragraph")]))
31855-
);
31856-
const table2 = schema2.nodes.table.create(null, [headerRow, emptyRow]);
31857-
let pos = $head.before($head.depth - 1);
31858-
for (let i = 0; i < myIndex - 1; i++) pos += grandParent.child(i).nodeSize;
31859-
const hStart = pos;
31860-
const sepEnd = hStart + headerBlock.nodeSize + parentNode3.nodeSize;
31861-
const tr = state.tr.replaceWith(hStart, sepEnd, table2);
31893+
const tableRange = findTableMarkdownRange(state);
31894+
if (!tableRange) return false;
31895+
const table2 = buildTableNode(tableRange.headerCells, tableRange.aligns);
31896+
const tr = state.tr.replaceWith(tableRange.from, tableRange.to, table2);
3186231897
view.dispatch(tr);
3186331898
event.preventDefault();
3186431899
return true;
@@ -32208,6 +32243,16 @@
3220832243
}
3220932244
});
3221032245
}
32246+
function compactLineBreakKeymap() {
32247+
return keymap({
32248+
"Mod-Enter": (state, dispatch) => {
32249+
const { $head } = state.selection;
32250+
if ($head.parent.type.name !== "paragraph") return false;
32251+
if (dispatch) dispatch(state.tr.insertText("\n"));
32252+
return true;
32253+
}
32254+
});
32255+
}
3221132256
function createPlugins() {
3221232257
return [
3221332258
slashMenuPlugin(),
@@ -32224,6 +32269,7 @@
3222432269
keymap(baseKeymap),
3222532270
buildBlockInputRules(),
3222632271
inlineMarkdownRules,
32272+
compactLineBreakKeymap(),
3222732273
codeBlockEscapeKeymap(),
3222832274
columnResizing(),
3222932275
tableEditing(),

0 commit comments

Comments
 (0)