Skip to content

Commit

Permalink
slate: improve list manipulation and restructure doc normalization to…
Browse files Browse the repository at this point in the history
… be easier to debug
  • Loading branch information
williamstein committed Apr 11, 2021
1 parent ecbf1d7 commit ca9bf69
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 58 deletions.
2 changes: 1 addition & 1 deletion src/smc-webapp/editors/slate/edit-bar/list-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const ListEdit: React.FC<Props> = ({ listProperties, editor }) => {
<InputNumber
title={"Numbered list starting value"}
size={"small"}
style={{ flex: 1, maxWidth: "6ex" }}
style={{ flex: 1, maxWidth: "8ex" }}
key={"start"}
min={0}
value={listProperties.start}
Expand Down
41 changes: 0 additions & 41 deletions src/smc-webapp/editors/slate/format/delete-backward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import { Range, Editor, Element, Path, Point, Text, Transforms } from "slate";
import { endswith } from "smc-util/misc";

export const withDeleteBackward = (editor) => {
const { deleteBackward } = editor;
Expand Down Expand Up @@ -39,52 +38,12 @@ function customDeleteBackwards(editor: Editor): boolean | undefined {
// Cursor is at the beginning of a non-paragraph block-level
// element, so maybe do something special.
switch (block.type) {
case "list_item":
deleteBackwardsInListItem(editor);
return true;
case "heading":
deleteBackwardsHeading(editor, block, path);
return true;
}
}

// Special handling inside a list item. This is complicated since
// the children of the list_item might include a paragraph, or could
// just directly be leaves.
function deleteBackwardsInListItem(editor: Editor) {
const immediate = Editor.above(editor, {
match: (node) => Editor.isBlock(editor, node),
});
if (immediate == null || !Element.isElement(immediate[0])) return;
if (immediate[0].type == "list_item") {
// Turn the list_item into a paragraph, which can live by itself:
Transforms.setNodes(editor, {
type: "paragraph",
});
} else {
// Make sure that tight isn't set on our paragraph that we're going
// to hoist out of this list, since tight is only useful inside
// a list.
Transforms.setNodes(editor, {
tight: undefined,
});
// It's a list_item that contains some other block element, so
// just unwrap which gets rid of the list_item, leaving a
// non-list-item block element, which can live on its own:
Transforms.unwrapNodes(editor, {
match: (node) => Element.isElement(node) && node.type == "list_item",
mode: "lowest",
});
}
// Then move up our newly free item by unwrapping it relative to
// the containing list. This may split the list into two lists.
Transforms.unwrapNodes(editor, {
match: (node) => Element.isElement(node) && endswith(node.type, "_list"),
split: true,
mode: "lowest",
});
}

// Special handling at beginning of heading.
function deleteBackwardsHeading(editor: Editor, block: Element, path: Path) {
if (Text.isText(block.children[0])) {
Expand Down
2 changes: 2 additions & 0 deletions src/smc-webapp/editors/slate/keyboard/backspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ function backspaceKey({ editor }) {
// This seems to work perfectly in all cases, including working around the
// void delete bug in Slate:
// https://github.com/ianstormtaylor/slate/issues/3875
// IMPORTANT: this editor.deleteBackward() is implemented in
// format/delete-backward.ts and is quite complicated!
editor.deleteBackward();
return true;
}
Expand Down
58 changes: 42 additions & 16 deletions src/smc-webapp/editors/slate/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,30 @@ import { getNodeAt } from "./slate-util";
import { emptyParagraph } from "./padding";
import { isListElement } from "./elements/list";

interface NormalizeInputs {
editor?: Editor;
node?: Node;
path?: Path;
}

type NormalizeFunction = (NormalizeInputs) => void;

const NORMALIZERS: NormalizeFunction[] = [];

export const withNormalize = (editor) => {
const { normalizeNode } = editor;

editor.normalizeNode = (entry) => {
const [node, path] = entry;

ensureListContainsListItems({ editor, node, path });
ensureListItemInAList({ editor, node, path });
trimLeadingWhitespace({ editor, node, path });
mergeAdjacentLists({ editor, node, path });
ensureDocumentNonempty({ editor });
for (const f of NORMALIZERS) {
//const before = JSON.stringify(editor.children);
f({ editor, node, path });
//const after = JSON.stringify(editor.children);
//if (before != after) {
// console.log(`${f.name}, BEFORE ${before}\n${f.name}, AFTER ${after}`);
//}
}

// Fall back to the original `normalizeNode` to enforce other constraints.
normalizeNode(entry);
Expand All @@ -41,14 +54,14 @@ export const withNormalize = (editor) => {
// don't put something in, then things immediately break due to
// selection assumptions. Slate doesn't do this automatically,
// since it doesn't nail down the internal format of a blank document.
function ensureDocumentNonempty({ editor }) {
NORMALIZERS.push(function ensureDocumentNonempty({ editor }) {
if (editor.children.length == 0) {
Editor.insertNode(editor, emptyParagraph());
}
}
});

// Ensure every list_item is contained in a list.
function ensureListItemInAList({ editor, node, path }) {
NORMALIZERS.push(function ensureListItemInAList({ editor, node, path }) {
if (Element.isElement(node) && node.type === "list_item") {
const [parent] = Editor.parent(editor, path);
if (!isListElement(parent)) {
Expand All @@ -58,10 +71,12 @@ function ensureListItemInAList({ editor, node, path }) {
});
}
}
}
});

// Ensure every immediate child of a list is a list_item.
function ensureListContainsListItems({ editor, node, path }) {
// Ensure every immediate child of a list is a list_item. Also, ensure
// that the children of each list_item are block level elements, since this
// makes list manipulation much easier and more consistent.
NORMALIZERS.push(function ensureListContainsListItems({ editor, node, path }) {
if (
Element.isElement(node) &&
(node.type === "bullet_list" || node.type == "ordered_list")
Expand All @@ -76,16 +91,27 @@ function ensureListContainsListItems({ editor, node, path }) {
});
return;
}
if (!Element.isElement(child.children[0])) {
// if the the children of the list item are leaves, wrap
// them all in a paragraph (for consistency with what our
// convertor from markdown does, and also our doc manipulation,
// e.g., backspace, assumes this).
Transforms.wrapNodes(editor, { type: "paragraph" } as Element, {
mode: "lowest",
match: (node) => !Element.isElement(node),
at: path.concat([i]),
});
}
i += 1;
}
}
}
});

/*
Trim *all* whitespace from the beginning of blocks whose first child is Text,
since markdown doesn't allow for it. (You can use &nbsp; of course.)
*/
function trimLeadingWhitespace({ editor, node, path }) {
NORMALIZERS.push(function trimLeadingWhitespace({ editor, node, path }) {
if (Element.isElement(node) && Text.isText(node.children[0])) {
const firstText = node.children[0].text;
if (firstText != null) {
Expand Down Expand Up @@ -113,13 +139,13 @@ function trimLeadingWhitespace({ editor, node, path }) {
}
}
}
}
});

/*
If there are two adjacent lists of the same type, merge the second one into
the first.
*/
function mergeAdjacentLists({ editor, node, path }) {
NORMALIZERS.push(function mergeAdjacentLists({ editor, node, path }) {
if (
Element.isElement(node) &&
(node.type === "bullet_list" || node.type === "ordered_list")
Expand All @@ -145,4 +171,4 @@ function mergeAdjacentLists({ editor, node, path }) {
}
} catch (_) {}
}
}
});

0 comments on commit ca9bf69

Please sign in to comment.